Bayesian GLMM Part4

Author

Murray Logan

Published

08/07/2025

1 Preparations

Load the necessary libraries

library(tidyverse) # for data wrangling
library(car) # for regression diagnostics
library(broom) # for tidy output
library(ggfortify) # for model diagnostics
library(knitr) # for kable
library(emmeans) # for estimating marginal means
library(MASS) # for glm.nb
library(brms)
library(broom.mixed)
library(tidybayes)
library(bayesplot)
library(standist) # for visualizing distributions
library(rstanarm)
library(cmdstanr) # for cmdstan
library(ggeffects)
library(rstan)
library(DHARMa)
library(ggridges)
library(easystats) # framework for stats, modelling and visualisation
library(patchwork)
library(modelsummary)
source("helperFunctions.R")

2 Scenario

Figure 1: Crab_shrimp_coral

To investigate synergistic coral defence by mutualist crustaceans, Mckeon et al. (2012) conducted an aquaria experiment in which colonies of a coral species were placed in a tank along with a predatory sea star and one of four symbiont combinations:

  • no symbiont,
  • a crab symbiont
  • a shrimp symbiont
  • both a crab and shrimp symbiont.

The experiments were conducted in a large octagonal flow-through seawater tank that was partitioned into eight sections, which thereby permitted two of each of the four symbiont combinations to be observed concurrently. The tank was left overnight and in the morning, the presence of feeding scars on each coral colony was scored as evidence of predation. The experiments were repeated ten times, each time with fresh coral colonies, sea stars and symbiont.

The ten experimental times represent blocks (random effects) within which the symbiont type (fixed effect) are nested.

3 Read in the data

mckeon <- read_csv("../data/mckeon.csv", trim_ws = TRUE)
Rows: 80 Columns: 3
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (1): SYMBIONT
dbl (2): BLOCK, PREDATION

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
glimpse(mckeon)
Rows: 80
Columns: 3
$ BLOCK     <dbl> 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10…
$ PREDATION <dbl> 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, …
$ SYMBIONT  <chr> "none", "none", "none", "none", "none", "none", "none", "non…
## Explore the first 6 rows of the data
head(mckeon)
# A tibble: 6 × 3
  BLOCK PREDATION SYMBIONT
  <dbl>     <dbl> <chr>   
1     1         0 none    
2     1         1 none    
3     2         1 none    
4     2         1 none    
5     3         1 none    
6     3         1 none    
str(mckeon)
spc_tbl_ [80 × 3] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
 $ BLOCK    : num [1:80] 1 1 2 2 3 3 4 4 5 5 ...
 $ PREDATION: num [1:80] 0 1 1 1 1 1 1 1 1 1 ...
 $ SYMBIONT : chr [1:80] "none" "none" "none" "none" ...
 - attr(*, "spec")=
  .. cols(
  ..   BLOCK = col_double(),
  ..   PREDATION = col_double(),
  ..   SYMBIONT = col_character()
  .. )
 - attr(*, "problems")=<externalptr> 
mckeon |> datawizard::data_codebook()
mckeon (80 rows and 3 variables, 3 shown)

ID | Name      | Type      | Missings |  Values |          N
---+-----------+-----------+----------+---------+-----------
1  | BLOCK     | numeric   | 0 (0.0%) | [1, 10] |         80
---+-----------+-----------+----------+---------+-----------
2  | PREDATION | numeric   | 0 (0.0%) |       0 | 30 (37.5%)
   |           |           |          |       1 | 50 (62.5%)
---+-----------+-----------+----------+---------+-----------
3  | SYMBIONT  | character | 0 (0.0%) |    both | 20 (25.0%)
   |           |           |          |   crabs | 20 (25.0%)
   |           |           |          |    none | 20 (25.0%)
   |           |           |          |  shrimp | 20 (25.0%)
------------------------------------------------------------

Since the response here is the presence or absence of predation (feeding scars), a binomial distribution is appropriate.

We need to make sure that the categorical variable and the random effect are defined as factors. When doing so, it might be valuable to rearrange the order of the fixed effect (SYMBIONT) such that the none group is considered the first group. This way, the other levels will all naturally be compared to this level (hence it will be treated as a reference of control group).

mckeon <- mckeon |>
  mutate(
    BLOCK = factor(BLOCK),
    SYMBIONT = factor(SYMBIONT, levels = c("none", "crabs", "shrimp", "both"))
  )

4 Exploratory data analysis

Model formula: \[ y_i \sim{} \mathcal{N}(n, p_i)\\ ln\left(\frac{p_i}{1-p_1}\right) =\boldsymbol{\beta} \bf{X_i} + \boldsymbol{\gamma} \bf{Z_i} \]

where \(\boldsymbol{\beta}\) and \(\boldsymbol{\gamma}\) are vectors of the fixed and random effects parameters respectively and \(\bf{X}\) is the model matrix representing the overall intercept and effects of symbionts on the probability of the colony experiencing predation. \(\bf{Z}\) represents a cell means model matrix for the random intercepts associated with individual coral colonies.

ggplot(mckeon, aes(y = PREDATION, x = SYMBIONT)) +
  geom_point(position = position_jitter(width = 0.2, height = 0)) +
  facet_wrap(~BLOCK)

5 Fit the model

In rstanarm, the default priors are designed to be weakly informative. They are chosen to provide moderate regularisation (to help prevent over-fitting) and help stabilise the computations.

mckeon.rstanarm <- stan_glmer(PREDATION ~ SYMBIONT + (1 | BLOCK),
  data = mckeon,
  family = binomial(link = "logit"),
  iter = 5000,
  warmup = 2000,
  chains = 3,
  thin = 5,
  refresh = 0,
  cores = 3
)
mckeon.rstanarm %>% prior_summary()
Priors for model '.' 
------
Intercept (after predictors centered)
 ~ normal(location = 0, scale = 2.5)

Coefficients
  Specified prior:
    ~ normal(location = [0,0,0], scale = [2.5,2.5,2.5])
  Adjusted prior:
    ~ normal(location = [0,0,0], scale = [5.74,5.74,5.74])

Covariance
 ~ decov(reg. = 1, conc. = 1, shape = 1, scale = 1)
------
See help('prior_summary.stanreg') for more details

This tells us:

  • for the intercept (when the family is binomial), a normal prior with a mean of 0 and a standard deviation of 2.5 is used. These are the default priors for bernoulli models. A mean of 0 on a logit scale corresponds to a probability of 0.5. Hence the prior expected value is 0.5.

  • for the coefficients (in this case, just the slope), the default prior is a normal prior centred around 0 with a standard deviation of 2.5. For binomial models, this is then adjusted for the scale of the data by dividing the 2.5 by the standard deviation of the predictor (then rounded).

model.matrix(~SYMBIONT, data = mckeon) %>%
  apply(2, sd) %>%
  (function(x) 2.5 / x)
   (Intercept)  SYMBIONTcrabs SYMBIONTshrimp   SYMBIONTboth 
           Inf       5.737305       5.737305       5.737305 

Lets now run with priors only so that we can explore the range of values they allow in the posteriors.

mckeon.rstanarm1 <- update(mckeon.rstanarm, prior_PD = TRUE)
mckeon.rstanarm1 %>%
  ggpredict() %>%
  plot(show_data = TRUE, jitter = c(0.5, 0))
You are calculating adjusted predictions on the population-level (i.e.
  `type = "fixed"`) for a *generalized* linear mixed model.
  This may produce biased estimates due to Jensen's inequality. Consider
  setting `bias_correction = TRUE` to correct for this bias.
  See also the documentation of the `bias_correction` argument.
Note: uncertainty of error terms are not taken into account. Consider
  setting `interval` to "prediction". This will call `posterior_predict()`
  instead of `posterior_epred()`.

Conclusions:

  • it is very difficult to assess the priors from the predicted posteriors when the posteriors are on the response scale as they will always approach values of 0 and 1 (no matter how wide they are on the link scale).

Interestingly, if we instead use ggemmeans, it will (perhaps erroneously) generate the partial effects on the link scale (yet with an incorrectly labelled y-axis). Probability scale values of 0.99 and 0.01 (very large and small respectively) correspond to value of approximately 4.6 and -4.6 respectively on the logit scale.

In the following partial plot, the routine attempts to convert the y-axes tick marks into percentages. Hence a value of 0.1 is converted to 10%. Consequently, values of -5 and 5 are labelled as -500% and 500% respectively. We know that on the probability scale, values must asymptote towards 0 and 1. Posterior intervals beyond -5 and 5 might be considered wide.

mckeon.rstanarm1 %>%
  ggemmeans(~SYMBIONT) %>%
  plot(show_data = TRUE, jitter = c(0.5, 0))

Alternatively, we could create the plot ourselves…

mckeon.rstanarm1 %>%
  emmeans(~SYMBIONT, type = "link") %>%
  as.data.frame() %>%
  ggplot(aes(y = emmean, x = SYMBIONT)) +
  geom_hline(yintercept = c(5, -5), linetype = "dashed") +
  geom_pointrange(aes(ymin = lower.HPD, ymax = upper.HPD)) +
  geom_point(
    data = mckeon, aes(y = PREDATION),
    position = position_jitter(width = 0.2, height = 0),
    alpha = 0.4, color = "red"
  )

The following link provides some guidance about defining priors. [https://github.com/stan-dev/stan/wiki/Prior-Choice-Recommendations]

When defining our own priors, we typically do not want them to be scaled.

If we wanted to define our own priors that were less vague, yet still not likely to bias the outcomes, we could try the following priors (mainly plucked out of thin air):

  • \(\beta_0\): normal centred at 0 (since this is 0.5 on the logit scale) with a standard deviation of 2
    • mean of 0: since logit(0.5)
    • alternatively, an argument could be made for a mean of 0.51: since logit(mean(mckeon$PREDATION))
    • sd of 2: since this not too large on logit scale
  • \(\beta_1\): normal centred at 0 with a standard deviation of 0.5
    • sd of 0.5: since model.matrix(~SYMBIONT, data=mckeon)[,-1] %>% apply(2,sd)
  • \(\Sigma\): decov with:
    • regularisation: the exponent for a LKJ prior on the correlation matrix. A value of 1 (default) implies a joint uniform prior
    • concentration: the concentration parameter for a symmetric Dirichlet distribution. A value of 1 (default) implies a joint uniform distribution
    • shape and scale: the shape and scale parameters for a gamma prior on the scale and scale parameters of the decov prior. A value of 1 for both (default) simplifies the gamma prior to a unit-exponential distribution.

I will also overlay the raw data for comparison.

mckeon.rstanarm2 <- stan_glmer(PREDATION ~ SYMBIONT + (1 | BLOCK),
  data = mckeon,
  family = binomial(link = "logit"),
  prior_intercept = normal(0, 2.5, autoscale = FALSE),
  prior = normal(0, 6, autoscale = FALSE),
  prior_covariance = decov(1, 1, 1, 1),
  prior_PD = TRUE,
  iter = 5000,
  warmup = 1000,
  chains = 3,
  thin = 5,
  refresh = 0
)
mckeon.rstanarm2 %>%
  ggpredict(~SYMBIONT) %>%
  plot(show_data = TRUE, jitter = c(0.5, 0))
Note: uncertainty of error terms are not taken into account. Consider
  setting `interval` to "prediction". This will call `posterior_predict()`
  instead of `posterior_epred()`.

mckeon.rstanarm2 %>%
  ggemmeans(~SYMBIONT) %>%
  plot(show_data = TRUE, jitter = c(0.5, 0))

Now lets refit, conditioning on the data.

mckeon.rstanarm3 <- update(mckeon.rstanarm2, prior_PD = FALSE)
posterior_vs_prior(mckeon.rstanarm3,
  color_by = "vs", group_by = TRUE,
  facet_args = list(scales = "free_y")
)

Drawing from prior...

Conclusions:

  • in each case, the prior is substantially wider than the posterior, suggesting that the posterior is not biased towards the prior.
ggemmeans(mckeon.rstanarm3, ~SYMBIONT) %>% plot(show_data = TRUE)
Data points may overlap. Use the `jitter` argument to add some amount of
  random variation to the location of data points and avoid overplotting.

ggpredict(mckeon.rstanarm3, ~SYMBIONT) %>% plot(show_data = TRUE)
Note: uncertainty of error terms are not taken into account. Consider
  setting `interval` to "prediction". This will call `posterior_predict()`
  instead of `posterior_epred()`.
Data points may overlap. Use the `jitter` argument to add some amount of
  random variation to the location of data points and avoid overplotting.

In brms, the default priors are designed to be weakly informative. They are chosen to provide moderate regularisation (to help prevent over fitting) and help stabilise the computations.

Unlike rstanarm, brms models must be compiled before they start sampling. For most models, the compilation of the stan code takes around 45 seconds.

mckeon.form <- bf(PREDATION | trials(1) ~ SYMBIONT + (1|BLOCK),
                  family=binomial(link='logit'))
#OR
mckeon.form <- bf(PREDATION ~ SYMBIONT + (1|BLOCK),
                  family=bernoulli(link='logit'))
options(width=150)
mckeon.form |> get_prior(data = mckeon)
                prior     class           coef group resp dpar nlpar lb ub       source
               (flat)         b                                                 default
               (flat)         b   SYMBIONTboth                             (vectorized)
               (flat)         b  SYMBIONTcrabs                             (vectorized)
               (flat)         b SYMBIONTshrimp                             (vectorized)
 student_t(3, 0, 2.5) Intercept                                                 default
 student_t(3, 0, 2.5)        sd                                       0         default
 student_t(3, 0, 2.5)        sd                BLOCK                  0    (vectorized)
 student_t(3, 0, 2.5)        sd      Intercept BLOCK                  0    (vectorized)
options(width=80)

The following link provides some guidance about defining priors. [https://github.com/stan-dev/stan/wiki/Prior-Choice-Recommendations]

When defining our own priors, we typically do not want them to be scaled.

If we wanted to define our own priors that were less vague, yet still not likely to bias the outcomes, we could try the following priors (mainly plucked out of thin air):

  • \(\beta_0\): normal centred at 0 (since this is 0.5 on the logit scale) with a standard deviation of 2.5
    • mean of 0: since logit(0.5)
    • sd of 2: since this not too large on logit scale
    • alternatively, we could potentially use logistic(0,1)
  • \(\beta_1\): normal centred at 0 with a standard deviation of 6
    • sd of 6: since 2.5/model.matrix(~SYMBIONT, data=mckeon) %>% apply(2,sd)
  • \(\sigma_j\): half-cauchy with parameters 0 and 2.
  • \(\Sigma\): decov with:
    • regularisation: the exponent for a LKJ prior on the correlation matrix. A value of 1 (default) implies a joint uniform prior
    • concentration: the concentration parameter for a symmetric Dirichlet distribution. A value of 1 (default) implies a joint uniform distribution
    • shape and scale: the shape and scale parameters for a gamma prior on the scale and scale parameters of the decov prior. A value of 1 for both (default) simplifies the gamma prior to a unit-exponential distribution.

Note, for hierarchical models, the model will tend to want to have a large \(sigma\) in order to fit the data better. It is a good idea to regularise this tendency by applying a prior that has most mass around zero. Suitable candidates include:

  • half-t: as the degrees of freedom approach infinity, this will approach a half-normal
  • half-cauchy: this is essentially a half-t with 1 degree of freedom
  • exponential

I will also overlay the raw data for comparison.

mckeon |>
  group_by(SYMBIONT) |>
  summarise(
    Mean = logit(mean(PREDATION)),
    Median = logit(median(PREDATION)),
    sd = logit(abs(sd(PREDATION)))
  )
# A tibble: 4 × 4
  SYMBIONT   Mean Median      sd
  <fct>     <dbl>  <dbl>   <dbl>
1 none      2.20     Inf -0.810 
2 crabs     0.405    Inf  0.0105
3 shrimp    0.201    Inf  0.0417
4 both     -0.201   -Inf  0.0417
2.5 / model.matrix(~SYMBIONT, data = mckeon) %>% apply(2, sd)
   (Intercept)  SYMBIONTcrabs SYMBIONTshrimp   SYMBIONTboth 
           Inf       5.737305       5.737305       5.737305 
mckeon |>
  group_by(SYMBIONT) |>
  summarise(
    mean_response = mean(PREDATION),
    mad_response = mad(PREDATION),
    sd_response = sd(PREDATION),
  ) |>
  mutate(
    mean_logit = logit(mean_response),
    # Delta method approximation
    sd_logit = sd_response / (mean_response * (1 - mean_response))
  )
# A tibble: 4 × 6
  SYMBIONT mean_response mad_response sd_response mean_logit sd_logit
  <fct>            <dbl>        <dbl>       <dbl>      <dbl>    <dbl>
1 none              0.9             0       0.308      2.20      3.42
2 crabs             0.6             0       0.503      0.405     2.09
3 shrimp            0.55            0       0.510      0.201     2.06
4 both              0.45            0       0.510     -0.201     2.06
standist::visualize("student_t(3, 0, 3.5)",
  "gamma(2,0.5)",
  "cauchy(0,2)",
  xlim = c(-10, 25)
)

mckeon.form <- bf(PREDATION | trials(1) ~ SYMBIONT + (1 | BLOCK),
  family = binomial(link = "logit")
)
# OR
mckeon.form <- bf(PREDATION ~ SYMBIONT + (1 | BLOCK),
  family = bernoulli(link = "logit")
)
get_prior(mckeon.form, data = mckeon)
                prior     class           coef group resp dpar nlpar lb ub
               (flat)         b                                           
               (flat)         b   SYMBIONTboth                            
               (flat)         b  SYMBIONTcrabs                            
               (flat)         b SYMBIONTshrimp                            
 student_t(3, 0, 2.5) Intercept                                           
 student_t(3, 0, 2.5)        sd                                       0   
 student_t(3, 0, 2.5)        sd                BLOCK                  0   
 student_t(3, 0, 2.5)        sd      Intercept BLOCK                  0   
       source
      default
 (vectorized)
 (vectorized)
 (vectorized)
      default
      default
 (vectorized)
 (vectorized)
priors <-
  prior(normal(0, 2.5), class = "Intercept") +
  prior(normal(0, 3), class = "b") +
  prior(student_t(3, 0, 1.5), class = "sd")

mckeon.brm2 <- brm(mckeon.form,
  data = mckeon,
  prior = priors,
  sample_prior = "only",
  iter = 5000,
  warmup = 1000,
  chains = 3, cores = 3,
  thin = 5,
  control = list(adapt_delta = 0.99, max_treedepth = 20),
  refresh = 0,
  backend = "rstan"
)
Compiling Stan program...
Start sampling
mckeon.brm2 %>%
  ggpredict(~SYMBIONT) %>%
  plot(show_data = TRUE)
Note: uncertainty of error terms are not taken into account. Consider
  setting `interval` to "prediction". This will call `posterior_predict()`
  instead of `posterior_epred()`.
Warning in glmmTMB::glmmTMB(formula = cond_formula, family = family, data =
data): some components missing from 'family': downstream methods may fail
Warning in glmmTMB::glmmTMB(formula = cond_formula, family = family, data =
data): some components missing from 'family': downstream methods may fail
Data points may overlap. Use the `jitter` argument to add some amount of
  random variation to the location of data points and avoid overplotting.

mckeon.brm2 %>%
  ggemmeans(~SYMBIONT) %>%
  plot(show_data = TRUE)
Warning in glmmTMB::glmmTMB(formula = cond_formula, family = family, data =
data): some components missing from 'family': downstream methods may fail
Warning in glmmTMB::glmmTMB(formula = cond_formula, family = family, data =
data): some components missing from 'family': downstream methods may fail
Warning in glmmTMB::glmmTMB(formula = cond_formula, family = family, data =
data): some components missing from 'family': downstream methods may fail
Warning in glmmTMB::glmmTMB(formula = cond_formula, family = family, data =
data): some components missing from 'family': downstream methods may fail
Data points may overlap. Use the `jitter` argument to add some amount of
  random variation to the location of data points and avoid overplotting.

mckeon.brm2 |>
  conditional_effects() |>
  plot(points = TRUE)

mckeon.brm2 %>%
  emmeans(~SYMBIONT, type = "link") %>%
  as.data.frame() %>%
  ggplot(aes(y = emmean, x = SYMBIONT)) +
  geom_hline(yintercept = c(5, -5), linetype = "dashed") +
  geom_pointrange(aes(ymin = lower.HPD, ymax = upper.HPD)) +
  geom_point(
    data = mckeon, aes(y = PREDATION),
    position = position_jitter(width = 0.2, height = 0),
    alpha = 0.4, color = "red"
  )
Warning: Method 'parse_bf' is deprecated. Please use 'brmsterms' instead.

Now lets refit, conditioning on the data.

mckeon.brm3 <- update(mckeon.brm2,
  sample_prior = "yes",
  cores = 3,
  refresh = 0
)
The desired updates require recompiling the model
Compiling Stan program...
Start sampling
mckeon.brm3 %>%
  ggpredict(~SYMBIONT) %>%
  plot(show_data = TRUE)
Note: uncertainty of error terms are not taken into account. Consider
  setting `interval` to "prediction". This will call `posterior_predict()`
  instead of `posterior_epred()`.
Warning in glmmTMB::glmmTMB(formula = cond_formula, family = family, data =
data): some components missing from 'family': downstream methods may fail
Warning in glmmTMB::glmmTMB(formula = cond_formula, family = family, data =
data): some components missing from 'family': downstream methods may fail
Data points may overlap. Use the `jitter` argument to add some amount of
  random variation to the location of data points and avoid overplotting.

mckeon.brm3 %>%
  ggemmeans(~SYMBIONT) %>%
  plot(show_data = TRUE)
Warning in glmmTMB::glmmTMB(formula = cond_formula, family = family, data =
data): some components missing from 'family': downstream methods may fail
Warning in glmmTMB::glmmTMB(formula = cond_formula, family = family, data =
data): some components missing from 'family': downstream methods may fail
Warning in glmmTMB::glmmTMB(formula = cond_formula, family = family, data =
data): some components missing from 'family': downstream methods may fail
Warning in glmmTMB::glmmTMB(formula = cond_formula, family = family, data =
data): some components missing from 'family': downstream methods may fail
Data points may overlap. Use the `jitter` argument to add some amount of
  random variation to the location of data points and avoid overplotting.

mckeon.brm3 |>
  conditional_effects() |>
  plot(points = TRUE)

mckeon.brm3 %>%
  emmeans(~SYMBIONT, type = "link") %>%
  as.data.frame() %>%
  ggplot(aes(y = emmean, x = SYMBIONT)) +
  geom_hline(yintercept = c(5, -5), linetype = "dashed") +
  geom_pointrange(aes(ymin = lower.HPD, ymax = upper.HPD)) +
  geom_point(
    data = mckeon, aes(y = PREDATION),
    position = position_jitter(width = 0.2, height = 0),
    alpha = 0.4, color = "red"
  )
Warning: Method 'parse_bf' is deprecated. Please use 'brmsterms' instead.

mckeon.brm3 %>% get_variables()
 [1] "b_Intercept"           "b_SYMBIONTcrabs"       "b_SYMBIONTshrimp"     
 [4] "b_SYMBIONTboth"        "sd_BLOCK__Intercept"   "Intercept"            
 [7] "r_BLOCK[1,Intercept]"  "r_BLOCK[2,Intercept]"  "r_BLOCK[3,Intercept]" 
[10] "r_BLOCK[4,Intercept]"  "r_BLOCK[5,Intercept]"  "r_BLOCK[6,Intercept]" 
[13] "r_BLOCK[7,Intercept]"  "r_BLOCK[8,Intercept]"  "r_BLOCK[9,Intercept]" 
[16] "r_BLOCK[10,Intercept]" "prior_Intercept"       "prior_b"              
[19] "prior_sd_BLOCK"        "lprior"                "lp__"                 
[22] "accept_stat__"         "stepsize__"            "treedepth__"          
[25] "n_leapfrog__"          "divergent__"           "energy__"             
mckeon.brm3 %>%
  hypothesis("SYMBIONTcrabs=0") %>%
  plot()

mckeon.brm3 %>%
  hypothesis("SYMBIONTshrimp=0") %>%
  plot()

mckeon.brm3 |> SUYR_prior_and_posterior()

While we are here, we might like to explore a random intercept/slope model

mckeon.form <- bf(PREDATION | trials(1) ~ SYMBIONT + (SYMBIONT | BLOCK),
  family = binomial(link = "logit")
)
# OR
mckeon.form <- bf(PREDATION ~ SYMBIONT + (SYMBIONT | BLOCK),
  family = bernoulli(link = "logit")
)
get_prior(mckeon.form, mckeon)
                prior     class           coef group resp dpar nlpar lb ub
               (flat)         b                                           
               (flat)         b   SYMBIONTboth                            
               (flat)         b  SYMBIONTcrabs                            
               (flat)         b SYMBIONTshrimp                            
               lkj(1)       cor                                           
               lkj(1)       cor                BLOCK                      
 student_t(3, 0, 2.5) Intercept                                           
 student_t(3, 0, 2.5)        sd                                       0   
 student_t(3, 0, 2.5)        sd                BLOCK                  0   
 student_t(3, 0, 2.5)        sd      Intercept BLOCK                  0   
 student_t(3, 0, 2.5)        sd   SYMBIONTboth BLOCK                  0   
 student_t(3, 0, 2.5)        sd  SYMBIONTcrabs BLOCK                  0   
 student_t(3, 0, 2.5)        sd SYMBIONTshrimp BLOCK                  0   
       source
      default
 (vectorized)
 (vectorized)
 (vectorized)
      default
 (vectorized)
      default
      default
 (vectorized)
 (vectorized)
 (vectorized)
 (vectorized)
 (vectorized)
## As there are not many observations, the following might be too ambitious without
## stronger priors
priors <-
  prior(normal(0, 2.5), class = "Intercept") +
  prior(normal(0, 3), class = "b") +
  ## prior(student_t(3,0,1.5), class = 'sd', coef =  "Intercept", group =  "BLOCK") +
  prior(student_t(3, 0, 1.5), class = "sd") +
  prior(lkj_corr_cholesky(1), class = "cor")

mckeon.brm4 <- brm(mckeon.form,
  data = mckeon,
  prior = priors,
  sample_prior = "yes",
  iter = 5000,
  warmup = 1000,
  chains = 3, cores = 3,
  thin = 5,
  refresh = 0,
  control = list(adapt_delta = 0.99, max_treedepth = 20),
  backend = "rstan"
)
Compiling Stan program...
Start sampling
mckeon.brm4 %>% get_variables()
 [1] "b_Intercept"                             
 [2] "b_SYMBIONTcrabs"                         
 [3] "b_SYMBIONTshrimp"                        
 [4] "b_SYMBIONTboth"                          
 [5] "sd_BLOCK__Intercept"                     
 [6] "sd_BLOCK__SYMBIONTcrabs"                 
 [7] "sd_BLOCK__SYMBIONTshrimp"                
 [8] "sd_BLOCK__SYMBIONTboth"                  
 [9] "cor_BLOCK__Intercept__SYMBIONTcrabs"     
[10] "cor_BLOCK__Intercept__SYMBIONTshrimp"    
[11] "cor_BLOCK__SYMBIONTcrabs__SYMBIONTshrimp"
[12] "cor_BLOCK__Intercept__SYMBIONTboth"      
[13] "cor_BLOCK__SYMBIONTcrabs__SYMBIONTboth"  
[14] "cor_BLOCK__SYMBIONTshrimp__SYMBIONTboth" 
[15] "Intercept"                               
[16] "r_BLOCK[1,Intercept]"                    
[17] "r_BLOCK[2,Intercept]"                    
[18] "r_BLOCK[3,Intercept]"                    
[19] "r_BLOCK[4,Intercept]"                    
[20] "r_BLOCK[5,Intercept]"                    
[21] "r_BLOCK[6,Intercept]"                    
[22] "r_BLOCK[7,Intercept]"                    
[23] "r_BLOCK[8,Intercept]"                    
[24] "r_BLOCK[9,Intercept]"                    
[25] "r_BLOCK[10,Intercept]"                   
[26] "r_BLOCK[1,SYMBIONTcrabs]"                
[27] "r_BLOCK[2,SYMBIONTcrabs]"                
[28] "r_BLOCK[3,SYMBIONTcrabs]"                
[29] "r_BLOCK[4,SYMBIONTcrabs]"                
[30] "r_BLOCK[5,SYMBIONTcrabs]"                
[31] "r_BLOCK[6,SYMBIONTcrabs]"                
[32] "r_BLOCK[7,SYMBIONTcrabs]"                
[33] "r_BLOCK[8,SYMBIONTcrabs]"                
[34] "r_BLOCK[9,SYMBIONTcrabs]"                
[35] "r_BLOCK[10,SYMBIONTcrabs]"               
[36] "r_BLOCK[1,SYMBIONTshrimp]"               
[37] "r_BLOCK[2,SYMBIONTshrimp]"               
[38] "r_BLOCK[3,SYMBIONTshrimp]"               
[39] "r_BLOCK[4,SYMBIONTshrimp]"               
[40] "r_BLOCK[5,SYMBIONTshrimp]"               
[41] "r_BLOCK[6,SYMBIONTshrimp]"               
[42] "r_BLOCK[7,SYMBIONTshrimp]"               
[43] "r_BLOCK[8,SYMBIONTshrimp]"               
[44] "r_BLOCK[9,SYMBIONTshrimp]"               
[45] "r_BLOCK[10,SYMBIONTshrimp]"              
[46] "r_BLOCK[1,SYMBIONTboth]"                 
[47] "r_BLOCK[2,SYMBIONTboth]"                 
[48] "r_BLOCK[3,SYMBIONTboth]"                 
[49] "r_BLOCK[4,SYMBIONTboth]"                 
[50] "r_BLOCK[5,SYMBIONTboth]"                 
[51] "r_BLOCK[6,SYMBIONTboth]"                 
[52] "r_BLOCK[7,SYMBIONTboth]"                 
[53] "r_BLOCK[8,SYMBIONTboth]"                 
[54] "r_BLOCK[9,SYMBIONTboth]"                 
[55] "r_BLOCK[10,SYMBIONTboth]"                
[56] "prior_Intercept"                         
[57] "prior_b"                                 
[58] "prior_sd_BLOCK"                          
[59] "prior_cor_BLOCK"                         
[60] "lprior"                                  
[61] "lp__"                                    
[62] "accept_stat__"                           
[63] "stepsize__"                              
[64] "treedepth__"                             
[65] "n_leapfrog__"                            
[66] "divergent__"                             
[67] "energy__"                                
mckeon.brm4 %>%
  hypothesis("SYMBIONTcrabs=0") %>%
  plot()

mckeon.brm4 %>%
  hypothesis("SYMBIONTshrimp=0") %>%
  plot()

mckeon.brm4 |> SUYR_prior_and_posterior()

mckeon.brm3 |> SUYR_prior_and_posterior()

mckeon.brm4 |>
  conditional_effects() |>
  plot(points = TRUE)

mckeon.brm3 |>
  conditional_effects() |>
  plot(points = TRUE)

mckeon.brm4 |> summary()
 Family: bernoulli 
  Links: mu = logit 
Formula: PREDATION ~ SYMBIONT + (SYMBIONT | BLOCK) 
   Data: mckeon (Number of observations: 80) 
  Draws: 3 chains, each with iter = 5000; warmup = 1000; thin = 5;
         total post-warmup draws = 2400

Multilevel Hyperparameters:
~BLOCK (Number of levels: 10) 
                                  Estimate Est.Error l-95% CI u-95% CI Rhat
sd(Intercept)                         2.17      1.23     0.22     5.00 1.00
sd(SYMBIONTcrabs)                     3.26      3.22     0.13    11.05 1.00
sd(SYMBIONTshrimp)                    2.36      2.11     0.10     7.67 1.00
sd(SYMBIONTboth)                      2.32      2.28     0.08     8.10 1.00
cor(Intercept,SYMBIONTcrabs)          0.30      0.39    -0.55     0.89 1.00
cor(Intercept,SYMBIONTshrimp)         0.26      0.41    -0.65     0.90 1.00
cor(SYMBIONTcrabs,SYMBIONTshrimp)     0.37      0.42    -0.62     0.92 1.01
cor(Intercept,SYMBIONTboth)           0.20      0.42    -0.68     0.88 1.00
cor(SYMBIONTcrabs,SYMBIONTboth)       0.28      0.42    -0.64     0.90 1.00
cor(SYMBIONTshrimp,SYMBIONTboth)      0.32      0.43    -0.68     0.92 1.00
                                  Bulk_ESS Tail_ESS
sd(Intercept)                         1694     1594
sd(SYMBIONTcrabs)                     1494     2196
sd(SYMBIONTshrimp)                    1988     2271
sd(SYMBIONTboth)                      2014     2200
cor(Intercept,SYMBIONTcrabs)          2103     2266
cor(Intercept,SYMBIONTshrimp)         2298     2181
cor(SYMBIONTcrabs,SYMBIONTshrimp)     1944     2144
cor(Intercept,SYMBIONTboth)           2345     2364
cor(SYMBIONTcrabs,SYMBIONTboth)       2091     2204
cor(SYMBIONTshrimp,SYMBIONTboth)      2152     2370

Regression Coefficients:
               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
Intercept          3.07      1.16     1.15     5.65 1.00     2055     2410
SYMBIONTcrabs     -1.43      1.68    -4.36     2.26 1.00     2110     2145
SYMBIONTshrimp    -2.19      1.46    -4.89     0.92 1.00     1775     1925
SYMBIONTboth      -3.32      1.45    -6.31    -0.38 1.00     2093     2178

Draws were sampled using sampling(NUTS). For each parameter, Bulk_ESS
and Tail_ESS are effective sample size measures, and Rhat is the potential
scale reduction factor on split chains (at convergence, Rhat = 1).
mckeon.brm4 %>%
  posterior_samples() %>%
  dplyr::select(-`lp__`) %>%
  pivot_longer(everything(), names_to = "key") %>%
  filter(!str_detect(key, "^r")) %>%
  mutate(
    Type = ifelse(str_detect(key, "prior"), "Prior", "Posterior"),
    ## Class = ifelse(str_detect(key, 'Intercept'),  'Intercept',
    ##         ifelse(str_detect(key, 'b'),  'b', 'sigma')),
    Class = case_when(
      str_detect(key, "(^b|^prior).*Intercept$") ~ "Intercept",
      str_detect(key, "b_SYMBIONT.*|prior_b_SYMBIONT.*") &
        !str_detect(key, ".*:.*") ~ "SYMBIONT",
      str_detect(key, "sd") ~ "sd",
      str_detect(key, "^cor|prior_cor") ~ "cor",
      str_detect(key, "sigma") ~ "sigma"
    ),
    Par = str_replace(key, "b_", "")
  ) %>%
  ggplot(aes(x = Type, y = value, color = Par)) +
  stat_pointinterval(position = position_dodge()) +
  facet_wrap(~Class, scales = "free")
Warning: Method 'posterior_samples' is deprecated. Please see ?as_draws for
recommended alternatives.

We can compare the two models using LOO

(l.1 <- mckeon.brm3 |> loo())
Warning: Found 1 observations with a pareto_k > 0.7 in model 'mckeon.brm3'. We
recommend to set 'moment_match = TRUE' in order to perform moment matching for
problematic observations.

Computed from 2400 by 80 log-likelihood matrix.

         Estimate   SE
elpd_loo    -27.8  6.9
p_loo         8.2  2.5
looic        55.5 13.9
------
MCSE of elpd_loo is NA.
MCSE and ESS estimates assume MCMC draws (r_eff in [0.8, 1.1]).

Pareto k diagnostic values:
                         Count Pct.    Min. ESS
(-Inf, 0.7]   (good)     79    98.8%   297     
   (0.7, 1]   (bad)       1     1.2%   <NA>    
   (1, Inf)   (very bad)  0     0.0%   <NA>    
See help('pareto-k-diagnostic') for details.
(l.2 <- mckeon.brm4 |> loo())
Warning: Found 4 observations with a pareto_k > 0.7 in model 'mckeon.brm4'. We
recommend to set 'moment_match = TRUE' in order to perform moment matching for
problematic observations.

Computed from 2400 by 80 log-likelihood matrix.

         Estimate   SE
elpd_loo    -26.4  6.5
p_loo        10.7  3.4
looic        52.7 13.0
------
MCSE of elpd_loo is NA.
MCSE and ESS estimates assume MCMC draws (r_eff in [0.7, 1.1]).

Pareto k diagnostic values:
                         Count Pct.    Min. ESS
(-Inf, 0.7]   (good)     76    95.0%   385     
   (0.7, 1]   (bad)       3     3.8%   <NA>    
   (1, Inf)   (very bad)  1     1.2%   <NA>    
See help('pareto-k-diagnostic') for details.
loo_compare(l.1, l.2)
            elpd_diff se_diff
mckeon.brm4  0.0       0.0   
mckeon.brm3 -1.4       1.2   

It appears that the random intercept/slope model is marginally better than the simpler random intercept model.

mckeon.brm4 %>%
  posterior_samples() %>%
  dplyr::select(-`lp__`) %>%
  pivot_longer(everything(), names_to = "key") %>%
  filter(!str_detect(key, "^r")) %>%
  mutate(
    Type = ifelse(str_detect(key, "prior"), "Prior", "Posterior"),
    Class = case_when(
      str_detect(key, "(^b|^prior).*Intercept$") ~ "Intercept",
      str_detect(key, "b_SYMBIONT.*|prior_b") ~ "TREATMENT",
      str_detect(key, "sd") ~ "sd",
      str_detect(key, "^cor|prior_cor") ~ "cor",
      str_detect(key, "sigma") ~ "sigma"
    ),
    Par = str_replace(key, "b_", "")
  ) %>%
  ggplot(aes(x = Type, y = value, color = Par)) +
  stat_pointinterval(position = position_dodge(), show.legend = FALSE) +
  facet_wrap(~Class, scales = "free")
Warning: Method 'posterior_samples' is deprecated. Please see ?as_draws for
recommended alternatives.

6 MCMC sampling diagnostics

The bayesplot package offers a range of MCMC diagnostics as well as Posterior Probability Checks (PPC), all of which have a convenient plot() interface. Lets start with the MCMC diagnostics.

available_mcmc()
bayesplot MCMC module:
  mcmc_acf
  mcmc_acf_bar
  mcmc_areas
  mcmc_areas_ridges
  mcmc_combo
  mcmc_dens
  mcmc_dens_chains
  mcmc_dens_overlay
  mcmc_hex
  mcmc_hist
  mcmc_hist_by_chain
  mcmc_intervals
  mcmc_neff
  mcmc_neff_hist
  mcmc_nuts_acceptance
  mcmc_nuts_divergence
  mcmc_nuts_energy
  mcmc_nuts_stepsize
  mcmc_nuts_treedepth
  mcmc_pairs
  mcmc_parcoord
  mcmc_rank_ecdf
  mcmc_rank_hist
  mcmc_rank_overlay
  mcmc_recover_hist
  mcmc_recover_intervals
  mcmc_recover_scatter
  mcmc_rhat
  mcmc_rhat_hist
  mcmc_scatter
  mcmc_trace
  mcmc_trace_highlight
  mcmc_violin

Of these, we will focus on:

  • trace: this plots the estimates of each parameter over the post-warmup length of each MCMC chain. Each chain is plotted in a different shade of blue, with each parameter in its own facet. Ideally, each trace should just look like noise without any discernible drift and each of the traces for a specific parameter should look the same (i.e, should not be displaced above or below any other trace for that parameter).
pars <- mckeon.brm3 |> get_variables()
pars <- pars %>%
  str_extract("^b.Intercept|^b_SYMBIONT.*|[sS]igma|^sd.*") %>%
  na.omit()
pars
[1] "b_Intercept"         "b_SYMBIONTcrabs"     "b_SYMBIONTshrimp"   
[4] "b_SYMBIONTboth"      "sd_BLOCK__Intercept"
attr(,"na.action")
 [1]  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
attr(,"class")
[1] "omit"
mckeon.brm3 %>% mcmc_plot(type = "trace", variables = pars)
Warning: The following arguments were unrecognized and ignored: variables
No divergences to plot.

# OR
mckeon.brm3 %>% mcmc_plot(
  type = "trace",
  variable = "^b.Intercept|^b_SYMBIONT.*|[sS]igma|^sd.*",
  regex = TRUE
)
No divergences to plot.

The chains appear well mixed and very similar

  • acf_bar (auto-correlation function): plots the auto-correlation between successive MCMC sample lags for each parameter and each chain
mckeon.brm3 %>% mcmc_plot(type = "acf_bar", variable = pars)

## OR
mckeon.brm3 %>% mcmc_plot(
  type = "acf_bar",
  variable = "^b.Intercept|^b_SYMBIONT.*|[sS]igma|^sd.*",
  regex = TRUE
)

There is no evidence of auto-correlation in the MCMC samples

  • rhat_hist: Rhat is a scale reduction factor measure of convergence between the chains. The closer the values are to 1, the more the chains have converged. Values greater than 1.05 indicate a lack of convergence. There will be an Rhat value for each parameter estimated.
mckeon.brm3 %>% mcmc_plot(type = "rhat_hist")
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

All Rhat values are below 1.05, suggesting the chains have converged.

  • neff_hist (number of effective samples): the ratio of the number of effective samples (those not rejected by the sampler) to the number of samples provides an indication of the effectiveness (and efficiency) of the MCMC sampler. Ratios that are less than 0.5 for a parameter suggest that the sampler spent considerable time in difficult areas of the sampling domain and rejected more than half of the samples (replacing them with the previous effective sample).

    If the ratios are low, tightening the priors may help.

mckeon.brm3 %>% mcmc_plot(type = "neff_hist")
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

Ratios all very high.

More diagnostics
mckeon.brm3 %>% mcmc_plot(type = "combo", variable = pars)

mckeon.brm3 %>% mcmc_plot(type = "violin", variable = pars)

The rstan package offers a range of MCMC diagnostics. Lets start with the MCMC diagnostics.

Of these, we will focus on:

  • stan_trace: this plots the estimates of each parameter over the post-warmup length of each MCMC chain. Each chain is plotted in a different colour, with each parameter in its own facet. Ideally, each trace should just look like noise without any discernible drift and each of the traces for a specific parameter should look the same (i.e, should not be displaced above or below any other trace for that parameter).
mckeon.brm4 |> get_variables()
 [1] "b_Intercept"                             
 [2] "b_SYMBIONTcrabs"                         
 [3] "b_SYMBIONTshrimp"                        
 [4] "b_SYMBIONTboth"                          
 [5] "sd_BLOCK__Intercept"                     
 [6] "sd_BLOCK__SYMBIONTcrabs"                 
 [7] "sd_BLOCK__SYMBIONTshrimp"                
 [8] "sd_BLOCK__SYMBIONTboth"                  
 [9] "cor_BLOCK__Intercept__SYMBIONTcrabs"     
[10] "cor_BLOCK__Intercept__SYMBIONTshrimp"    
[11] "cor_BLOCK__SYMBIONTcrabs__SYMBIONTshrimp"
[12] "cor_BLOCK__Intercept__SYMBIONTboth"      
[13] "cor_BLOCK__SYMBIONTcrabs__SYMBIONTboth"  
[14] "cor_BLOCK__SYMBIONTshrimp__SYMBIONTboth" 
[15] "Intercept"                               
[16] "r_BLOCK[1,Intercept]"                    
[17] "r_BLOCK[2,Intercept]"                    
[18] "r_BLOCK[3,Intercept]"                    
[19] "r_BLOCK[4,Intercept]"                    
[20] "r_BLOCK[5,Intercept]"                    
[21] "r_BLOCK[6,Intercept]"                    
[22] "r_BLOCK[7,Intercept]"                    
[23] "r_BLOCK[8,Intercept]"                    
[24] "r_BLOCK[9,Intercept]"                    
[25] "r_BLOCK[10,Intercept]"                   
[26] "r_BLOCK[1,SYMBIONTcrabs]"                
[27] "r_BLOCK[2,SYMBIONTcrabs]"                
[28] "r_BLOCK[3,SYMBIONTcrabs]"                
[29] "r_BLOCK[4,SYMBIONTcrabs]"                
[30] "r_BLOCK[5,SYMBIONTcrabs]"                
[31] "r_BLOCK[6,SYMBIONTcrabs]"                
[32] "r_BLOCK[7,SYMBIONTcrabs]"                
[33] "r_BLOCK[8,SYMBIONTcrabs]"                
[34] "r_BLOCK[9,SYMBIONTcrabs]"                
[35] "r_BLOCK[10,SYMBIONTcrabs]"               
[36] "r_BLOCK[1,SYMBIONTshrimp]"               
[37] "r_BLOCK[2,SYMBIONTshrimp]"               
[38] "r_BLOCK[3,SYMBIONTshrimp]"               
[39] "r_BLOCK[4,SYMBIONTshrimp]"               
[40] "r_BLOCK[5,SYMBIONTshrimp]"               
[41] "r_BLOCK[6,SYMBIONTshrimp]"               
[42] "r_BLOCK[7,SYMBIONTshrimp]"               
[43] "r_BLOCK[8,SYMBIONTshrimp]"               
[44] "r_BLOCK[9,SYMBIONTshrimp]"               
[45] "r_BLOCK[10,SYMBIONTshrimp]"              
[46] "r_BLOCK[1,SYMBIONTboth]"                 
[47] "r_BLOCK[2,SYMBIONTboth]"                 
[48] "r_BLOCK[3,SYMBIONTboth]"                 
[49] "r_BLOCK[4,SYMBIONTboth]"                 
[50] "r_BLOCK[5,SYMBIONTboth]"                 
[51] "r_BLOCK[6,SYMBIONTboth]"                 
[52] "r_BLOCK[7,SYMBIONTboth]"                 
[53] "r_BLOCK[8,SYMBIONTboth]"                 
[54] "r_BLOCK[9,SYMBIONTboth]"                 
[55] "r_BLOCK[10,SYMBIONTboth]"                
[56] "prior_Intercept"                         
[57] "prior_b"                                 
[58] "prior_sd_BLOCK"                          
[59] "prior_cor_BLOCK"                         
[60] "lprior"                                  
[61] "lp__"                                    
[62] "accept_stat__"                           
[63] "stepsize__"                              
[64] "treedepth__"                             
[65] "n_leapfrog__"                            
[66] "divergent__"                             
[67] "energy__"                                
pars <- mckeon.brm4 |> get_variables()
pars <- str_extract(pars, "^b_.*|^sigma$|^sd.*") |> na.omit()

mckeon.brm4$fit |>
  stan_trace(pars = pars)

## mckeon.brm3$fit |> stan_trace()

The chains appear well mixed and very similar

  • stan_acf (auto-correlation function): plots the auto-correlation between successive MCMC sample lags for each parameter and each chain
mckeon.brm4$fit |>
  stan_ac(pars = pars)

## mckeon.brm3$fit |> stan_ac()

There is no evidence of auto-correlation in the MCMC samples

  • stan_rhat: Rhat is a scale reduction factor measure of convergence between the chains. The closer the values are to 1, the more the chains have converged. Values greater than 1.05 indicate a lack of convergence. There will be an Rhat value for each parameter estimated.
mckeon.brm4$fit |> stan_rhat()
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

All Rhat values are below 1.05, suggesting the chains have converged.

  • stan_ess (number of effective samples): the ratio of the number of effective samples (those not rejected by the sampler) to the number of samples provides an indication of the effectiveness (and efficiency) of the MCMC sampler. Ratios that are less than 0.5 for a parameter suggest that the sampler spent considerable time in difficult areas of the sampling domain and rejected more than half of the samples (replacing them with the previous effective sample).

    If the ratios are low, tightening the priors may help.

mckeon.brm4$fit |> stan_ess()
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

Ratios all very high.

mckeon.brm4$fit |>
  stan_dens(separate_chains = TRUE, pars = pars)

The ggmean package also as a set of MCMC diagnostic functions. Lets start with the MCMC diagnostics.

Of these, we will focus on:

  • ggs_traceplot: this plots the estimates of each parameter over the post-warmup length of each MCMC chain. Each chain is plotted in a different colour, with each parameter in its own facet. Ideally, each trace should just look like noise without any discernible drift and each of the traces for a specific parameter should look the same (i.e, should not be displaced above or below any other trace for that parameter).
## mckeon.ggs <- mckeon.brm3 %>% ggs(burnin = FALSE, inc_warmup = FALSE)
## mckeon.ggs %>% ggs_traceplot()

The chains appear well mixed and very similar

  • gss_autocorrelation (autocorrelation function): plots the autocorrelation between successive MCMC sample lags for each parameter and each chain
## ggs_autocorrelation(mckeon.ggs)

There is no evidence of auto-correlation in the MCMC samples

  • stan_rhat: Rhat is a scale reduction factor measure of convergence between the chains. The closer the values are to 1, the more the chains have converged. Values greater than 1.05 indicate a lack of convergence. There will be an Rhat value for each parameter estimated.
## ggs_Rhat(mckeon.ggs)

All Rhat values are below 1.05, suggesting the chains have converged.

  • stan_ess (number of effective samples): the ratio of the number of effective samples (those not rejected by the sampler) to the number of samples provides an indication of the effectiveness (and efficiency) of the MCMC sampler. Ratios that are less than 0.5 for a parameter suggest that the sampler spent considerable time in difficult areas of the sampling domain and rejected more than half of the samples (replacing them with the previous effective sample).

    If the ratios are low, tightening the priors may help.

## ggs_effective(mckeon.ggs)

Ratios all very high.

More diagnostics
## ggs_crosscorrelation(mckeon.ggs)
## ggs_grb(mckeon.ggs)

7 Model validation

Post predictive checks provide additional diagnostics about the fit of the model. Specifically, they provide a comparison between predictions drawn from the model and the observed data used to train the model.

available_ppc()
bayesplot PPC module:
  ppc_bars
  ppc_bars_grouped
  ppc_boxplot
  ppc_dens
  ppc_dens_overlay
  ppc_dens_overlay_grouped
  ppc_ecdf_overlay
  ppc_ecdf_overlay_grouped
  ppc_error_binned
  ppc_error_hist
  ppc_error_hist_grouped
  ppc_error_scatter
  ppc_error_scatter_avg
  ppc_error_scatter_avg_grouped
  ppc_error_scatter_avg_vs_x
  ppc_freqpoly
  ppc_freqpoly_grouped
  ppc_hist
  ppc_intervals
  ppc_intervals_grouped
  ppc_km_overlay
  ppc_km_overlay_grouped
  ppc_loo_intervals
  ppc_loo_pit_ecdf
  ppc_loo_pit_overlay
  ppc_loo_pit_qq
  ppc_loo_ribbon
  ppc_pit_ecdf
  ppc_pit_ecdf_grouped
  ppc_ribbon
  ppc_ribbon_grouped
  ppc_rootogram
  ppc_scatter
  ppc_scatter_avg
  ppc_scatter_avg_grouped
  ppc_stat
  ppc_stat_2d
  ppc_stat_freqpoly
  ppc_stat_freqpoly_grouped
  ppc_stat_grouped
  ppc_violin_grouped
  • dens_overlay: plots the density distribution of the observed data (black line) overlayed on top of 50 density distributions generated from draws from the model (light blue). Ideally, the 50 realisations should be roughly consistent with the observed data.
mckeon.brm4 |> pp_check(type = "dens_overlay", nsamples = 250)
Warning: Argument 'nsamples' is deprecated. Please use argument 'ndraws'
instead.

The model draws appear to be consistent with the observed data.

  • error_scatter_avg: this plots the observed values against the average residuals. Similar to a residual plot, we do not want to see any patterns in this plot. Note, this is not really that useful for models that involve a binomial response
mckeon.brm4 %>% pp_check(type = "error_scatter_avg")
Using all posterior draws for ppc type 'error_scatter_avg' by default.

This is not really interpretable

  • intervals: plots the observed data overlayed on top of posterior predictions associated with each level of the predictor. Ideally, the observed data should all fall within the predictive intervals.
mckeon.brm4 %>% pp_check(group = "BLOCK", type = "intervals")
Using all posterior draws for ppc type 'intervals' by default.
Warning: The following arguments were unrecognized and ignored: group

The shinystan package allows the full suite of MCMC diagnostics and posterior predictive checks to be accessed via a web interface.

# library(shinystan)
# launch_shinystan(mckeon.brm2)

DHARMa residuals provide very useful diagnostics. Unfortunately, we cannot directly use the simulateResiduals() function to generate the simulated residuals. However, if we are willing to calculate some of the components yourself, we can still obtain the simulated residuals from the fitted stan model.

We need to supply:

  • simulated (predicted) responses associated with each observation.
  • observed values
  • fitted (predicted) responses (averaged) associated with each observation
preds <- mckeon.brm3 %>% posterior_predict(nsamples = 250, summary = FALSE)
Warning: Argument 'nsamples' is deprecated. Please use argument 'ndraws'
instead.
mckeon.resids <- createDHARMa(
  simulatedResponse = t(preds),
  observedResponse = mckeon$PREDATION,
  fittedPredictedResponse = apply(preds, 2, median),
  integerResponse = TRUE
)
plot(mckeon.resids, quantreg = TRUE)

mckeon.resids <- make_brms_dharma_res(mckeon.brm4, integerResponse = FALSE)
wrap_elements(~ testUniformity(mckeon.resids)) +
  wrap_elements(~ plotResiduals(mckeon.resids, form = factor(rep(1, nrow(mckeon))))) +
  wrap_elements(~ plotResiduals(mckeon.resids, quantreg = FALSE)) +
  wrap_elements(~ testDispersion(mckeon.resids))

Conclusions:

  • the simulated residuals do not suggest any issues with the residuals
  • there is no evidence of a lack of fit.

8 Partial effects plots

mckeon.brm4 |>
  conditional_effects() |>
  plot(points = TRUE)

mckeon.brm4 |>
  ggpredict() |>
  plot(show_data = TRUE, jitter = c(0.5, 0))
Note: uncertainty of error terms are not taken into account. Consider
  setting `interval` to "prediction". This will call `posterior_predict()`
  instead of `posterior_epred()`.
Warning in glmmTMB::glmmTMB(formula = cond_formula, family = family, data =
data): some components missing from 'family': downstream methods may fail
Warning in glmmTMB::glmmTMB(formula = cond_formula, family = family, data =
data): some components missing from 'family': downstream methods may fail

mckeon.brm4 |>
  ggemmeans(~SYMBIONT) |>
  plot(show_data = TRUE, jitter = c(0.5, 0))
Warning in glmmTMB::glmmTMB(formula = cond_formula, family = family, data =
data): some components missing from 'family': downstream methods may fail
Warning in glmmTMB::glmmTMB(formula = cond_formula, family = family, data =
data): some components missing from 'family': downstream methods may fail
Warning in glmmTMB::glmmTMB(formula = cond_formula, family = family, data =
data): some components missing from 'family': downstream methods may fail
Warning in glmmTMB::glmmTMB(formula = cond_formula, family = family, data =
data): some components missing from 'family': downstream methods may fail

## Partial residuals in binomial models are too confusing for the average viewer
## as they will yeild values that are not exactly 0 or 1 and this seems wrong.
## Partial.obs <- mckeon.brm3$data %>%
##     mutate(Pred = predict(mckeon.brm3, re.form=NA)[,'Estimate'],
##            Resid = resid(mckeon.brm3)[,'Estimate'],
##            Obs = Pred + Resid)

mckeon.brm4 |>
  epred_draws(newdata = mckeon, re_formula = NA) |>
  median_hdci() |>
  ggplot(aes(x = SYMBIONT, y = .epred)) +
  geom_pointrange(aes(ymin = .lower, ymax = .upper)) +
  geom_line() +
  geom_point(
    data = mckeon, aes(y = PREDATION, x = SYMBIONT),
    alpha = 0.2,
    position = position_jitter(width = 0.2, height = 0)
  )

9 Model investigation

The summary() method generates simple summaries (mean, standard deviation as well as 10, 50 and 90 percentiles).

mckeon.brm4 |> summary()
 Family: bernoulli 
  Links: mu = logit 
Formula: PREDATION ~ SYMBIONT + (SYMBIONT | BLOCK) 
   Data: mckeon (Number of observations: 80) 
  Draws: 3 chains, each with iter = 5000; warmup = 1000; thin = 5;
         total post-warmup draws = 2400

Multilevel Hyperparameters:
~BLOCK (Number of levels: 10) 
                                  Estimate Est.Error l-95% CI u-95% CI Rhat
sd(Intercept)                         2.17      1.23     0.22     5.00 1.00
sd(SYMBIONTcrabs)                     3.26      3.22     0.13    11.05 1.00
sd(SYMBIONTshrimp)                    2.36      2.11     0.10     7.67 1.00
sd(SYMBIONTboth)                      2.32      2.28     0.08     8.10 1.00
cor(Intercept,SYMBIONTcrabs)          0.30      0.39    -0.55     0.89 1.00
cor(Intercept,SYMBIONTshrimp)         0.26      0.41    -0.65     0.90 1.00
cor(SYMBIONTcrabs,SYMBIONTshrimp)     0.37      0.42    -0.62     0.92 1.01
cor(Intercept,SYMBIONTboth)           0.20      0.42    -0.68     0.88 1.00
cor(SYMBIONTcrabs,SYMBIONTboth)       0.28      0.42    -0.64     0.90 1.00
cor(SYMBIONTshrimp,SYMBIONTboth)      0.32      0.43    -0.68     0.92 1.00
                                  Bulk_ESS Tail_ESS
sd(Intercept)                         1694     1594
sd(SYMBIONTcrabs)                     1494     2196
sd(SYMBIONTshrimp)                    1988     2271
sd(SYMBIONTboth)                      2014     2200
cor(Intercept,SYMBIONTcrabs)          2103     2266
cor(Intercept,SYMBIONTshrimp)         2298     2181
cor(SYMBIONTcrabs,SYMBIONTshrimp)     1944     2144
cor(Intercept,SYMBIONTboth)           2345     2364
cor(SYMBIONTcrabs,SYMBIONTboth)       2091     2204
cor(SYMBIONTshrimp,SYMBIONTboth)      2152     2370

Regression Coefficients:
               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
Intercept          3.07      1.16     1.15     5.65 1.00     2055     2410
SYMBIONTcrabs     -1.43      1.68    -4.36     2.26 1.00     2110     2145
SYMBIONTshrimp    -2.19      1.46    -4.89     0.92 1.00     1775     1925
SYMBIONTboth      -3.32      1.45    -6.31    -0.38 1.00     2093     2178

Draws were sampled using sampling(NUTS). For each parameter, Bulk_ESS
and Tail_ESS are effective sample size measures, and Rhat is the potential
scale reduction factor on split chains (at convergence, Rhat = 1).

Conclusions:

  • the coefficients are presented on a logit scale. Whilst this is not relevant for the purpose of inference testing, it does make it difficult to interpret the coefficients.
  • if we exponentiate the coefficients (\(log(\frac{\rho}{1-\rho})\) -> \(\frac{\rho}{1-\rho}\)), they will be presented on a odds ratio scale, and thus:
    • the intercept (none symbionts) will be 21.46. That is, corals without a symbiont are 21.46 times more likely to be predated on than not predated on. The odds of predation in this the absence of symbionts is 21.46:1.
    • in the presence of a crab symbiont, the odds of being predated on are only 0.24 times that of the none symbiont group. That is, in the presence of a crab symbiont, the odds of predation decline by 76%.
    • in the presence of a shrimp symbiont, the odds of being predated on are only 0.11 times that of the none symbiont group. That is, in the presence of a shrimp symbiont, the odds of predation decline by 89%.
    • in the presence of both crab and shrimp symbionts, the odds of being predated on are only 0.04 times that of the none symbiont group. That is, in the presence of both crab and shrimp symbiont, the odds of predation decline by 96%.
  • if we back-transform the intercept full to the response scale (probability scale), (\(log(\frac{\rho}{1-\rho})\) -> \(\rho\)), the intercept is interpreted as the probability that corals will be predated in the absence of of symbionts is 1
# A draws_df: 800 iterations, 3 chains, and 61 variables
   b_Intercept b_SYMBIONTcrabs b_SYMBIONTshrimp b_SYMBIONTboth
1          3.5            2.91            -2.13           -2.9
2          2.3            0.50            -1.80           -2.6
3          1.7            1.82            -1.84           -2.1
4          5.0           -1.92            -1.41           -4.6
5          3.7           -4.39            -4.48           -3.1
6          1.7            1.71            -0.48           -3.7
7          2.6           -1.63            -4.25           -6.4
8          3.5           -2.78            -2.82           -4.2
9          2.5           -0.33            -1.52           -5.2
10         3.1           -3.13            -3.22           -3.9
   sd_BLOCK__Intercept sd_BLOCK__SYMBIONTcrabs sd_BLOCK__SYMBIONTshrimp
1                 1.57                    5.32                     0.81
2                 0.88                    8.85                     1.09
3                 1.96                   16.11                     1.91
4                 4.47                    0.52                     2.11
5                 7.95                    0.51                     2.76
6                 1.07                    9.47                     1.64
7                 1.72                    0.75                     0.55
8                 1.40                    2.86                     1.00
9                 0.76                    2.92                     2.09
10                2.18                    3.96                     0.30
   sd_BLOCK__SYMBIONTboth
1                    2.66
2                    0.35
3                    1.07
4                    0.53
5                    6.35
6                    1.48
7                    2.72
8                    1.46
9                    3.46
10                   1.23
# ... with 2390 more draws, and 53 more variables
# ... hidden reserved variables {'.chain', '.iteration', '.draw'}
variable median lower upper Pl Pg rhat ess_bulk ess_tail
b_Intercept 19.4898474 0.6045049 1.632308e+02 0.0004167 0.9995833 0.9997882 2055.092 2409.901
b_SYMBIONTcrabs 0.2070936 0.0004330 4.507043e+00 0.8150000 0.1850000 1.0001200 2110.146 2145.482
b_SYMBIONTshrimp 0.1051186 0.0007097 1.359671e+00 0.9295833 0.0704167 1.0013649 1775.340 1925.299
b_SYMBIONTboth 0.0353588 0.0001376 3.982936e-01 0.9841667 0.0158333 1.0013479 2092.725 2178.352
sd_BLOCK__Intercept 7.7858769 1.0017286 7.386592e+01 0.0000000 1.0000000 1.0016444 1694.269 1594.410
sd_BLOCK__SYMBIONTcrabs 11.6013806 1.0039661 5.348673e+03 0.0000000 1.0000000 1.0031484 1494.461 2196.065
sd_BLOCK__SYMBIONTshrimp 6.2717197 1.0000851 5.115885e+02 0.0000000 1.0000000 1.0018677 1987.527 2271.175
sd_BLOCK__SYMBIONTboth 5.5787951 1.0000267 6.373033e+02 0.0000000 1.0000000 1.0002518 2013.781 2200.406
cor_BLOCK__Intercept__SYMBIONTcrabs 1.4361448 0.5467804 2.387722e+00 0.2245833 0.7754167 1.0024862 2102.692 2265.638
cor_BLOCK__Intercept__SYMBIONTshrimp 1.3695621 0.4908625 2.427816e+00 0.2575000 0.7425000 1.0008267 2297.981 2180.753
cor_BLOCK__SYMBIONTcrabs__SYMBIONTshrimp 1.5872139 0.5341720 2.511116e+00 0.1887500 0.8112500 1.0052500 1944.669 2143.888
cor_BLOCK__Intercept__SYMBIONTboth 1.2642661 0.4438061 2.282491e+00 0.3154167 0.6845833 1.0014627 2345.564 2363.818
cor_BLOCK__SYMBIONTcrabs__SYMBIONTboth 1.4063701 0.4789396 2.371325e+00 0.2433333 0.7566667 1.0002358 2091.271 2203.523
cor_BLOCK__SYMBIONTshrimp__SYMBIONTboth 1.5022249 0.4712434 2.470391e+00 0.2329167 0.7670833 0.9999984 2152.382 2369.526
Intercept 3.6362598 0.1230073 2.050830e+01 0.0791667 0.9208333 1.0015257 2286.548 2326.427
r_BLOCK[1,Intercept] 0.0815708 0.0003289 9.481156e-01 0.9620833 0.0379167 1.0005518 1715.449 1854.252
r_BLOCK[2,Intercept] 0.2062181 0.0003063 1.482728e+00 0.8883333 0.1116667 1.0007042 1853.909 2368.630
r_BLOCK[3,Intercept] 0.2137460 0.0001339 1.410428e+00 0.8950000 0.1050000 1.0018816 1885.827 2174.560
r_BLOCK[4,Intercept] 0.2243993 0.0002903 1.373790e+00 0.8970833 0.1029167 0.9992068 1839.651 2167.104
r_BLOCK[5,Intercept] 0.8984007 0.0034043 7.156857e+00 0.5462500 0.4537500 1.0009166 2120.588 2164.507
r_BLOCK[6,Intercept] 2.5170598 0.0326588 4.035546e+01 0.1937500 0.8062500 0.9996354 1866.804 2225.130
r_BLOCK[7,Intercept] 7.0092390 0.0435089 5.542687e+02 0.0920833 0.9079167 1.0003418 2000.375 2421.948
r_BLOCK[8,Intercept] 6.7055315 0.0978663 6.794157e+02 0.0875000 0.9125000 1.0004743 2004.067 2288.873
r_BLOCK[9,Intercept] 6.4276932 0.0443778 5.131682e+02 0.0841667 0.9158333 1.0033917 1865.168 1893.327
r_BLOCK[10,Intercept] 1.7970720 0.0065568 2.163980e+01 0.3000000 0.7000000 1.0003953 1944.647 2123.695
r_BLOCK[1,SYMBIONTcrabs] 0.1091017 0.0000000 1.804867e+00 0.8600000 0.1400000 1.0014140 1731.908 2368.207
r_BLOCK[2,SYMBIONTcrabs] 0.1035740 0.0000000 1.463403e+00 0.8787500 0.1212500 0.9997432 1917.953 2454.353
r_BLOCK[3,SYMBIONTcrabs] 0.1107821 0.0000000 1.458813e+00 0.8754167 0.1245833 1.0022731 1662.701 2250.793
r_BLOCK[4,SYMBIONTcrabs] 0.1003052 0.0000000 1.573084e+00 0.8675000 0.1325000 1.0014566 1867.693 2093.095
r_BLOCK[5,SYMBIONTcrabs] 1.6696494 0.0028625 3.883661e+02 0.3270833 0.6729167 1.0028783 2237.067 2289.464
r_BLOCK[6,SYMBIONTcrabs] 2.2984975 0.0050040 3.589090e+03 0.2729167 0.7270833 1.0005871 2087.195 2492.996
r_BLOCK[7,SYMBIONTcrabs] 3.7527496 0.0001332 4.147813e+04 0.2262500 0.7737500 0.9996911 2150.970 2315.729
r_BLOCK[8,SYMBIONTcrabs] 3.9132804 0.0000887 5.426021e+04 0.2200000 0.7800000 1.0014707 2161.248 2450.557
r_BLOCK[9,SYMBIONTcrabs] 4.1275947 0.0002635 4.291756e+04 0.2145833 0.7854167 1.0004711 1997.156 2305.570
r_BLOCK[10,SYMBIONTcrabs] 3.8991113 0.0048181 3.505546e+04 0.1966667 0.8033333 0.9998870 2203.243 2234.398
r_BLOCK[1,SYMBIONTshrimp] 0.2292724 0.0000000 2.207386e+00 0.8116667 0.1883333 1.0009479 1902.731 2158.368
r_BLOCK[2,SYMBIONTshrimp] 0.2351541 0.0000000 1.810802e+00 0.8266667 0.1733333 1.0003215 2033.008 2179.685
r_BLOCK[3,SYMBIONTshrimp] 0.2302034 0.0000000 1.643125e+00 0.8354167 0.1645833 1.0009086 1694.365 1979.737
r_BLOCK[4,SYMBIONTshrimp] 0.2211076 0.0000000 1.648815e+00 0.8391667 0.1608333 1.0016496 1941.789 2124.145
r_BLOCK[5,SYMBIONTshrimp] 0.8118627 0.0003214 6.443777e+00 0.6016667 0.3983333 0.9996430 2219.672 2215.179
r_BLOCK[6,SYMBIONTshrimp] 1.9783094 0.0009300 3.803152e+02 0.2758333 0.7241667 1.0003573 2084.789 2287.016
r_BLOCK[7,SYMBIONTshrimp] 2.6036762 0.0003320 3.030357e+03 0.2325000 0.7675000 1.0002810 2073.144 2329.077
r_BLOCK[8,SYMBIONTshrimp] 2.4391834 0.0036593 2.437851e+03 0.2425000 0.7575000 1.0013801 1956.243 2187.507
r_BLOCK[9,SYMBIONTshrimp] 2.6367647 0.0092494 3.348415e+03 0.2333333 0.7666667 1.0009221 1907.435 2084.156
r_BLOCK[10,SYMBIONTshrimp] 2.8906273 0.0217982 2.925741e+03 0.1937500 0.8062500 1.0009027 2041.581 2222.254
r_BLOCK[1,SYMBIONTboth] 0.3580806 0.0000000 2.985766e+00 0.7495833 0.2504167 1.0013357 1990.443 2066.486
r_BLOCK[2,SYMBIONTboth] 0.3497297 0.0000000 2.339799e+00 0.7779167 0.2220833 1.0005476 2139.529 2230.296
r_BLOCK[3,SYMBIONTboth] 0.3589003 0.0000000 2.440805e+00 0.7741667 0.2258333 0.9995410 2051.749 2160.395
r_BLOCK[4,SYMBIONTboth] 0.3675690 0.0000000 2.360844e+00 0.7779167 0.2220833 1.0004158 2072.553 2147.404
r_BLOCK[5,SYMBIONTboth] 0.5125846 0.0000000 2.439201e+00 0.7545833 0.2454167 0.9996884 2285.842 2370.382
r_BLOCK[6,SYMBIONTboth] 0.9835666 0.0000040 1.083872e+01 0.5166667 0.4833333 1.0010776 2411.078 2369.730
r_BLOCK[7,SYMBIONTboth] 2.5339575 0.0057559 2.622432e+03 0.2366667 0.7633333 0.9995157 2189.615 2260.810
r_BLOCK[8,SYMBIONTboth] 2.6504183 0.0018615 1.946967e+03 0.2370833 0.7629167 1.0017945 2059.261 2090.619
r_BLOCK[9,SYMBIONTboth] 2.5818989 0.0020219 2.103280e+03 0.2391667 0.7608333 1.0002435 2032.213 2366.702
r_BLOCK[10,SYMBIONTboth] 3.2837192 0.0307971 1.626532e+03 0.1733333 0.8266667 1.0003176 2132.097 2160.038
prior_Intercept 1.0544982 0.0000472 7.027993e+01 0.4908333 0.5091667 0.9999369 2537.456 2433.688
prior_b 0.7794361 0.0000589 1.381056e+02 0.5279167 0.4720833 0.9999476 1827.060 2164.417
prior_sd_BLOCK 3.1241502 1.0018341 1.487606e+02 0.0000000 1.0000000 1.0002197 2556.861 2394.074
prior_cor_BLOCK 1.0190180 0.3863294 2.062882e+00 0.4845833 0.5154167 0.9995468 2368.598 2328.842
lprior 0.0000000 0.0000000 0.000000e+00 1.0000000 0.0000000 1.0016575 1935.319 2222.751
lp__ 0.0000000 0.0000000 0.000000e+00 1.0000000 0.0000000 1.0004049 2272.767 2270.003
Warning: Dropping 'draws_df' class as required metadata was removed.
variable median lower upper Pl Pg rhat ess_bulk ess_tail
b_Intercept 19.4898474 0.6045049 163.2307858 0.0004167 0.9995833 1.0007028 2036.007 2402.110
b_SYMBIONTcrabs 0.2070936 0.0004330 4.5070426 0.8150000 0.1850000 1.0006845 2097.514 2156.583
b_SYMBIONTshrimp 0.1051186 0.0007097 1.3596712 0.9295833 0.0704167 1.0012607 1750.729 1855.157
b_SYMBIONTboth 0.0353588 0.0001376 0.3982936 0.9841667 0.0158333 1.0009438 2073.024 2109.557
sd_BLOCK__Intercept 7.7858769 1.0017286 73.8659205 0.0000000 1.0000000 1.0009991 1679.408 1558.746
sd_BLOCK__SYMBIONTcrabs 11.6013806 1.0039661 5348.6733889 0.0000000 1.0000000 0.9999464 1476.002 2167.789
sd_BLOCK__SYMBIONTshrimp 6.2717197 1.0000851 511.5884505 0.0000000 1.0000000 0.9997040 1965.188 2181.840
sd_BLOCK__SYMBIONTboth 5.5787951 1.0000267 637.3032532 0.0000000 1.0000000 1.0001018 2004.852 2193.817
mckeon.brm4$fit |>
  tidyMCMC(
    estimate.method = "median",
    conf.int = TRUE, conf.method = "HPDinterval",
    rhat = TRUE, ess = TRUE
  )
# A tibble: 60 × 7
   term                        estimate std.error conf.low conf.high  rhat   ess
   <chr>                          <dbl>     <dbl>    <dbl>     <dbl> <dbl> <int>
 1 b_Intercept                    3.07      1.16   9.42e-1     5.34  1.000  2000
 2 b_SYMBIONTcrabs               -1.43      1.68  -4.57e+0     1.93  1.00   2133
 3 b_SYMBIONTshrimp              -2.19      1.46  -5.13e+0     0.665 1.00   1763
 4 b_SYMBIONTboth                -3.32      1.45  -6.44e+0    -0.553 1.00   2105
 5 sd_BLOCK__Intercept            2.17      1.23   3.56e-2     4.32  1.00   1772
 6 sd_BLOCK__SYMBIONTcrabs        3.26      3.22   3.96e-3     8.58  1.00   2050
 7 sd_BLOCK__SYMBIONTshrimp       2.36      2.11   8.51e-5     6.24  1.000  1974
 8 sd_BLOCK__SYMBIONTboth         2.32      2.28   2.67e-5     6.46  1.00   2007
 9 cor_BLOCK__Intercept__SYMB…    0.304     0.385 -4.23e-1     0.950 1.00   2027
10 cor_BLOCK__Intercept__SYMB…    0.263     0.412 -5.12e-1     0.969 1.00   2277
# ℹ 50 more rows

Conclusions:

see above

Due to the presence of a log transform in the predictor, it is better to use the regex version.

mckeon.brm4 |> get_variables()
 [1] "b_Intercept"                             
 [2] "b_SYMBIONTcrabs"                         
 [3] "b_SYMBIONTshrimp"                        
 [4] "b_SYMBIONTboth"                          
 [5] "sd_BLOCK__Intercept"                     
 [6] "sd_BLOCK__SYMBIONTcrabs"                 
 [7] "sd_BLOCK__SYMBIONTshrimp"                
 [8] "sd_BLOCK__SYMBIONTboth"                  
 [9] "cor_BLOCK__Intercept__SYMBIONTcrabs"     
[10] "cor_BLOCK__Intercept__SYMBIONTshrimp"    
[11] "cor_BLOCK__SYMBIONTcrabs__SYMBIONTshrimp"
[12] "cor_BLOCK__Intercept__SYMBIONTboth"      
[13] "cor_BLOCK__SYMBIONTcrabs__SYMBIONTboth"  
[14] "cor_BLOCK__SYMBIONTshrimp__SYMBIONTboth" 
[15] "Intercept"                               
[16] "r_BLOCK[1,Intercept]"                    
[17] "r_BLOCK[2,Intercept]"                    
[18] "r_BLOCK[3,Intercept]"                    
[19] "r_BLOCK[4,Intercept]"                    
[20] "r_BLOCK[5,Intercept]"                    
[21] "r_BLOCK[6,Intercept]"                    
[22] "r_BLOCK[7,Intercept]"                    
[23] "r_BLOCK[8,Intercept]"                    
[24] "r_BLOCK[9,Intercept]"                    
[25] "r_BLOCK[10,Intercept]"                   
[26] "r_BLOCK[1,SYMBIONTcrabs]"                
[27] "r_BLOCK[2,SYMBIONTcrabs]"                
[28] "r_BLOCK[3,SYMBIONTcrabs]"                
[29] "r_BLOCK[4,SYMBIONTcrabs]"                
[30] "r_BLOCK[5,SYMBIONTcrabs]"                
[31] "r_BLOCK[6,SYMBIONTcrabs]"                
[32] "r_BLOCK[7,SYMBIONTcrabs]"                
[33] "r_BLOCK[8,SYMBIONTcrabs]"                
[34] "r_BLOCK[9,SYMBIONTcrabs]"                
[35] "r_BLOCK[10,SYMBIONTcrabs]"               
[36] "r_BLOCK[1,SYMBIONTshrimp]"               
[37] "r_BLOCK[2,SYMBIONTshrimp]"               
[38] "r_BLOCK[3,SYMBIONTshrimp]"               
[39] "r_BLOCK[4,SYMBIONTshrimp]"               
[40] "r_BLOCK[5,SYMBIONTshrimp]"               
[41] "r_BLOCK[6,SYMBIONTshrimp]"               
[42] "r_BLOCK[7,SYMBIONTshrimp]"               
[43] "r_BLOCK[8,SYMBIONTshrimp]"               
[44] "r_BLOCK[9,SYMBIONTshrimp]"               
[45] "r_BLOCK[10,SYMBIONTshrimp]"              
[46] "r_BLOCK[1,SYMBIONTboth]"                 
[47] "r_BLOCK[2,SYMBIONTboth]"                 
[48] "r_BLOCK[3,SYMBIONTboth]"                 
[49] "r_BLOCK[4,SYMBIONTboth]"                 
[50] "r_BLOCK[5,SYMBIONTboth]"                 
[51] "r_BLOCK[6,SYMBIONTboth]"                 
[52] "r_BLOCK[7,SYMBIONTboth]"                 
[53] "r_BLOCK[8,SYMBIONTboth]"                 
[54] "r_BLOCK[9,SYMBIONTboth]"                 
[55] "r_BLOCK[10,SYMBIONTboth]"                
[56] "prior_Intercept"                         
[57] "prior_b"                                 
[58] "prior_sd_BLOCK"                          
[59] "prior_cor_BLOCK"                         
[60] "lprior"                                  
[61] "lp__"                                    
[62] "accept_stat__"                           
[63] "stepsize__"                              
[64] "treedepth__"                             
[65] "n_leapfrog__"                            
[66] "divergent__"                             
[67] "energy__"                                
mckeon.draw <- mckeon.brm4 |>
  gather_draws(`b.Intercept.*|b_SYMBIONT.*`, regex = TRUE)
mckeon.draw
# A tibble: 9,600 × 5
# Groups:   .variable [4]
   .chain .iteration .draw .variable   .value
    <int>      <int> <int> <chr>        <dbl>
 1      1          1     1 b_Intercept   3.54
 2      1          2     2 b_Intercept   2.33
 3      1          3     3 b_Intercept   1.69
 4      1          4     4 b_Intercept   5.00
 5      1          5     5 b_Intercept   3.70
 6      1          6     6 b_Intercept   1.70
 7      1          7     7 b_Intercept   2.61
 8      1          8     8 b_Intercept   3.49
 9      1          9     9 b_Intercept   2.52
10      1         10    10 b_Intercept   3.10
# ℹ 9,590 more rows

We can then summarise this

mckeon.draw |> median_hdci()
# A tibble: 4 × 7
  .variable        .value .lower .upper .width .point .interval
  <chr>             <dbl>  <dbl>  <dbl>  <dbl> <chr>  <chr>    
1 b_Intercept        2.97  0.998  5.43    0.95 median hdci     
2 b_SYMBIONTboth    -3.34 -6.49  -0.601   0.95 median hdci     
3 b_SYMBIONTcrabs   -1.57 -4.42   2.10    0.95 median hdci     
4 b_SYMBIONTshrimp  -2.25 -4.97   0.831   0.95 median hdci     
## On a odd ratio scale
mckeon.draw |>
  mutate(.value = exp(.value)) |>
  median_hdci()
# A tibble: 4 × 7
  .variable         .value   .lower  .upper .width .point .interval
  <chr>              <dbl>    <dbl>   <dbl>  <dbl> <chr>  <chr>    
1 b_Intercept      19.5    0.605    163.      0.95 median hdci     
2 b_SYMBIONTboth    0.0354 0.000138   0.394   0.95 median hdci     
3 b_SYMBIONTcrabs   0.207  0.000433   4.50    0.95 median hdci     
4 b_SYMBIONTshrimp  0.105  0.000710   1.36    0.95 median hdci     
mckeon.brm4 |>
  gather_draws(`b_Intercept.*|b_SYMBIONT.*`, regex = TRUE) |>
  ggplot() +
  geom_vline(xintercept = 0, linetype = "dashed") +
  stat_slab(aes(
    x = .value, y = .variable,
    fill = stat(ggdist::cut_cdf_qi(cdf,
      .width = c(0.5, 0.8, 0.95),
      labels = scales::percent_format()
    ))
  ), color = "black") +
  scale_fill_brewer("Interval", direction = -1, na.translate = FALSE)
Warning: `stat(ggdist::cut_cdf_qi(cdf, .width = c(0.5, 0.8, 0.95), labels =
scales::percent_format()))` was deprecated in ggplot2 3.4.0.
ℹ Please use `after_stat(ggdist::cut_cdf_qi(cdf, .width = c(0.5, 0.8, 0.95),
  labels = scales::percent_format()))` instead.

mckeon.brm4 |>
  gather_draws(`.Intercept.*|b_SYMBIONT.*`, regex = TRUE) |>
  ggplot() +
  geom_vline(xintercept = 0, linetype = "dashed") +
  stat_halfeye(aes(x = .value, y = .variable)) +
  theme_classic()

mckeon.brm4$fit |> plot(type = "intervals")
'pars' not specified. Showing first 10 parameters by default.
ci_level: 0.8 (80% intervals)
outer_level: 0.95 (95% intervals)

mckeon.brm4 |>
  gather_draws(`^b_.*`, regex = TRUE) |>
  filter(.variable != "b_Intercept") |>
  ggplot() +
  stat_halfeye(aes(x = .value, y = .variable)) +
  facet_wrap(~.variable, scales = "free")

mckeon.brm4 |>
  gather_draws(`^b_.*`, regex = TRUE) |>
  filter(.variable != "b_Intercept") |>
  ggplot() +
  stat_halfeye(aes(x = .value, y = .variable)) +
  geom_vline(xintercept = 0, linetype = "dashed")

mckeon.brm4 |>
  gather_draws(`^b_.*`, regex = TRUE) |>
  filter(.variable != "b_Intercept") |>
  ggplot() +
  geom_density_ridges(aes(x = .value, y = .variable), alpha = 0.4) +
  geom_vline(xintercept = 0, linetype = "dashed")
Picking joint bandwidth of 0.275

## Or in colour
mckeon.brm4 |>
  gather_draws(`^b_.*`, regex = TRUE) |>
  filter(.variable != "b_Intercept") |>
  ggplot() +
  geom_density_ridges_gradient(
    aes(
      x = (.value),
      y = .variable,
      fill = stat(x)
    ),
    alpha = 0.4, colour = "white",
    quantile_lines = TRUE,
    quantiles = c(0.025, 0.975)
  ) +
  geom_vline(xintercept = 1, linetype = "dashed") +
  scale_x_continuous() +
  scale_fill_viridis_c(option = "C")
Picking joint bandwidth of 0.275

## Fractional scale
mckeon.brm4 |>
  gather_draws(`^b_.*`, regex = TRUE) |>
  filter(.variable != "b_Intercept") |>
  ggplot() +
  geom_density_ridges_gradient(
    aes(
      x = exp(.value),
      y = .variable,
      fill = stat(x)
    ),
    alpha = 0.4, colour = "white",
    quantile_lines = TRUE,
    quantiles = c(0.025, 0.975)
  ) +
  geom_vline(xintercept = 1, linetype = "dashed") +
  scale_x_continuous(trans = scales::log2_trans()) +
  scale_fill_viridis_c(option = "C")
Picking joint bandwidth of 0.397

mckeon.brm4 |> tidy_draws()
# A tibble: 2,400 × 70
   .chain .iteration .draw b_Intercept b_SYMBIONTcrabs b_SYMBIONTshrimp
    <int>      <int> <int>       <dbl>           <dbl>            <dbl>
 1      1          1     1        3.54           2.91            -2.13 
 2      1          2     2        2.33           0.500           -1.80 
 3      1          3     3        1.69           1.82            -1.84 
 4      1          4     4        5.00          -1.92            -1.41 
 5      1          5     5        3.70          -4.39            -4.48 
 6      1          6     6        1.70           1.71            -0.478
 7      1          7     7        2.61          -1.63            -4.25 
 8      1          8     8        3.49          -2.78            -2.82 
 9      1          9     9        2.52          -0.332           -1.52 
10      1         10    10        3.10          -3.13            -3.22 
# ℹ 2,390 more rows
# ℹ 64 more variables: b_SYMBIONTboth <dbl>, sd_BLOCK__Intercept <dbl>,
#   sd_BLOCK__SYMBIONTcrabs <dbl>, sd_BLOCK__SYMBIONTshrimp <dbl>,
#   sd_BLOCK__SYMBIONTboth <dbl>, cor_BLOCK__Intercept__SYMBIONTcrabs <dbl>,
#   cor_BLOCK__Intercept__SYMBIONTshrimp <dbl>,
#   cor_BLOCK__SYMBIONTcrabs__SYMBIONTshrimp <dbl>,
#   cor_BLOCK__Intercept__SYMBIONTboth <dbl>, …
mckeon.brm4 |> spread_draws(`.*Intercept.*|b_SYMBIONT.*`, regex = TRUE)
# A tibble: 2,400 × 23
   .chain .iteration .draw b_Intercept b_SYMBIONTcrabs b_SYMBIONTshrimp
    <int>      <int> <int>       <dbl>           <dbl>            <dbl>
 1      1          1     1        3.54           2.91            -2.13 
 2      1          2     2        2.33           0.500           -1.80 
 3      1          3     3        1.69           1.82            -1.84 
 4      1          4     4        5.00          -1.92            -1.41 
 5      1          5     5        3.70          -4.39            -4.48 
 6      1          6     6        1.70           1.71            -0.478
 7      1          7     7        2.61          -1.63            -4.25 
 8      1          8     8        3.49          -2.78            -2.82 
 9      1          9     9        2.52          -0.332           -1.52 
10      1         10    10        3.10          -3.13            -3.22 
# ℹ 2,390 more rows
# ℹ 17 more variables: b_SYMBIONTboth <dbl>, sd_BLOCK__Intercept <dbl>,
#   cor_BLOCK__Intercept__SYMBIONTcrabs <dbl>,
#   cor_BLOCK__Intercept__SYMBIONTshrimp <dbl>,
#   cor_BLOCK__Intercept__SYMBIONTboth <dbl>, Intercept <dbl>,
#   `r_BLOCK[1,Intercept]` <dbl>, `r_BLOCK[2,Intercept]` <dbl>,
#   `r_BLOCK[3,Intercept]` <dbl>, `r_BLOCK[4,Intercept]` <dbl>, …
mckeon.brm4 |>
  posterior_samples() |>
  as_tibble()
Warning: Method 'posterior_samples' is deprecated. Please see ?as_draws for
recommended alternatives.
# A tibble: 2,400 × 61
   b_Intercept b_SYMBIONTcrabs b_SYMBIONTshrimp b_SYMBIONTboth
         <dbl>           <dbl>            <dbl>          <dbl>
 1        3.54           2.91            -2.13           -2.89
 2        2.33           0.500           -1.80           -2.65
 3        1.69           1.82            -1.84           -2.08
 4        5.00          -1.92            -1.41           -4.58
 5        3.70          -4.39            -4.48           -3.07
 6        1.70           1.71            -0.478          -3.69
 7        2.61          -1.63            -4.25           -6.37
 8        3.49          -2.78            -2.82           -4.16
 9        2.52          -0.332           -1.52           -5.20
10        3.10          -3.13            -3.22           -3.87
# ℹ 2,390 more rows
# ℹ 57 more variables: sd_BLOCK__Intercept <dbl>,
#   sd_BLOCK__SYMBIONTcrabs <dbl>, sd_BLOCK__SYMBIONTshrimp <dbl>,
#   sd_BLOCK__SYMBIONTboth <dbl>, cor_BLOCK__Intercept__SYMBIONTcrabs <dbl>,
#   cor_BLOCK__Intercept__SYMBIONTshrimp <dbl>,
#   cor_BLOCK__SYMBIONTcrabs__SYMBIONTshrimp <dbl>,
#   cor_BLOCK__Intercept__SYMBIONTboth <dbl>, …
mckeon.brm4 |>
  bayes_R2(re.form = NA, summary = FALSE) |>
  median_hdci()
         y        ymin      ymax .width .point .interval
1 0.174463 0.004768317 0.3150586   0.95 median      hdci
mckeon.brm4 |>
  bayes_R2(re.form = ~ (1 | BLOCK), summary = FALSE) |>
  median_hdci()
          y      ymin     ymax .width .point .interval
1 0.4774058 0.1171811 0.730853   0.95 median      hdci
## if we had random intercept/slope
mckeon.brm4 |>
  bayes_R2(re.form = ~ (SYMBIONT | BLOCK), summary = FALSE) |>
  median_hdci()
          y      ymin      ymax .width .point .interval
1 0.7037594 0.5693019 0.8187142   0.95 median      hdci
mckeon.brm4 |> modelsummary(
  statistic = c("conf.low", "conf.high"),
  shape = term ~ statistic,
  exponentiate = FALSE
)
Warning: 
`modelsummary` uses the `performance` package to extract goodness-of-fit
statistics from models of this class. You can specify the statistics you wish
to compute by supplying a `metrics` argument to `modelsummary`, which will then
push it forward to `performance`. Acceptable values are: "all", "common",
"none", or a character vector of metrics names. For example: `modelsummary(mod,
metrics = c("RMSE", "R2")` Note that some metrics are computationally
expensive. See `?performance::performance` for details.
 This warning appears once per session.
(1)
Est. 2.5 % 97.5 %
b_Intercept 2.970 1.149 5.646
b_SYMBIONTcrabs -1.575 -4.358 2.264
b_SYMBIONTshrimp -2.253 -4.891 0.924
b_SYMBIONTboth -3.342 -6.312 -0.384
sd_BLOCK__Intercept 2.052 0.223 4.996
sd_BLOCK__SYMBIONTcrabs 2.451 0.132 11.049
sd_BLOCK__SYMBIONTshrimp 1.836 0.102 7.668
sd_BLOCK__SYMBIONTboth 1.719 0.078 8.101
cor_BLOCK__Intercept__SYMBIONTcrabs 0.362 -0.551 0.891
cor_BLOCK__Intercept__SYMBIONTshrimp 0.314 -0.652 0.903
cor_BLOCK__SYMBIONTcrabs__SYMBIONTshrimp 0.462 -0.624 0.923
cor_BLOCK__Intercept__SYMBIONTboth 0.234 -0.682 0.875
cor_BLOCK__SYMBIONTcrabs__SYMBIONTboth 0.341 -0.644 0.896
cor_BLOCK__SYMBIONTshrimp__SYMBIONTboth 0.407 -0.676 0.924
Num.Obs. 80
R2 0.704
R2 Marg. 0.174
ICC 0.9
ELPD -26.4
ELPD s.e. 6.5
LOOIC 52.7
LOOIC s.e. 13.0
WAIC 50.0
RMSE 0.21
mckeon.brm4 |> modelplot(exponentiate = FALSE)

10 Further analyses

In addition to comparing each of the symbiont types against the control group of no symbionts, it might be interesting to investigate whether there are any differences between the predation protection provided by crabs and shrimp, as well as whether having both crabs and shrimp symbionts is different to only a single symbiont type.

These contrasts can be explored via specific contrasts.

Table 1: Contrast coefficients
SYMBIONT Crab vs Shrimp One vs Both None vs Symbiont
none 0 0 1
crab 1 1/2 -1/3
shrimp -1 1/2 -1/3
both 0 -1 -1/3
mckeon.brm4 |>
  emmeans(~SYMBIONT, type = "response") |>
  pairs()
Warning: Method 'parse_bf' is deprecated. Please use 'brmsterms' instead.
 contrast       odds.ratio lower.HPD upper.HPD
 none / crabs         4.83   0.00179      50.9
 none / shrimp        9.51   0.01256      82.5
 none / both         28.28   0.01766     306.4
 crabs / shrimp       1.95   0.00104      45.7
 crabs / both         6.12   0.00825     138.2
 shrimp / both        2.99   0.00397      43.5

Point estimate displayed: median 
Results are back-transformed from the log odds ratio scale 
HPD interval probability: 0.95 
# via tidy_draws
mckeon.em <-
  mckeon.brm4 |>
  emmeans(~SYMBIONT, type = "link") |>
  pairs() |>
  tidy_draws() |>
  mutate(across(starts_with("contrast"), exp)) |>
  summarise_draws(median,
    HDInterval::hdi,
    Pl = ~ mean(.x < 1),
    Pg = ~ mean(.x > 1)
  )
Warning: Method 'parse_bf' is deprecated. Please use 'brmsterms' instead.
mckeon.em
# A tibble: 6 × 6
  variable                median   lower upper     Pl    Pg
  <chr>                    <dbl>   <dbl> <dbl>  <dbl> <dbl>
1 contrast none - crabs     4.83 0.00179  50.9 0.185  0.815
2 contrast none - shrimp    9.51 0.0126   82.5 0.0704 0.930
3 contrast none - both     28.3  0.0177  306.  0.0158 0.984
4 contrast crabs - shrimp   1.95 0.00104  45.7 0.334  0.666
5 contrast crabs - both     6.12 0.00825 138.  0.117  0.883
6 contrast shrimp - both    2.99 0.00397  43.5 0.218  0.782
# or via gather_emmeans_draws()
mckeon.em <-
  mckeon.brm4 |>
  emmeans(~SYMBIONT, type = "link") |>
  pairs() |>
  gather_emmeans_draws() |>
  mutate(.value = exp(.value)) |>
  summarise(median_hdci(.value),
    Pl = mean(.value < 1),
    Pg = mean(.value > 1)
  )
Warning: Method 'parse_bf' is deprecated. Please use 'brmsterms' instead.
mckeon.em
# A tibble: 6 × 9
  contrast           y    ymin  ymax .width .point .interval     Pl    Pg
  <chr>          <dbl>   <dbl> <dbl>  <dbl> <chr>  <chr>      <dbl> <dbl>
1 crabs - both    6.12 0.00825 138.    0.95 median hdci      0.117  0.883
2 crabs - shrimp  1.95 0.00104  45.5   0.95 median hdci      0.334  0.666
3 none - both    28.3  0.0177  306.    0.95 median hdci      0.0158 0.984
4 none - crabs    4.83 0.00179  50.8   0.95 median hdci      0.185  0.815
5 none - shrimp   9.51 0.0126   82.0   0.95 median hdci      0.0704 0.930
6 shrimp - both   2.99 0.00397  43.4   0.95 median hdci      0.218  0.782
## On a probability scale
mckeon.em <-
  mckeon.brm4 |>
  emmeans(~SYMBIONT, type = "link") |>
  regrid() |>
  pairs() |>
  tidy_draws() |>
  summarise_draws(median,
    HDInterval::hdi,
    Pl = ~ mean(.x < 0),
    Pg = ~ mean(.x > 0)
  )
Warning: Method 'parse_bf' is deprecated. Please use 'brmsterms' instead.
mckeon.em
# A tibble: 6 × 6
  variable                median   lower upper     Pl    Pg
  <chr>                    <dbl>   <dbl> <dbl>  <dbl> <dbl>
1 contrast none - crabs   0.121  -0.114  0.643 0.185  0.815
2 contrast none - shrimp  0.245  -0.0717 0.740 0.0704 0.930
3 contrast none - both    0.494   0.0469 0.896 0.0158 0.984
4 contrast crabs - shrimp 0.0822 -0.434  0.619 0.334  0.666
5 contrast crabs - both   0.307  -0.216  0.835 0.117  0.883
6 contrast shrimp - both  0.206  -0.293  0.742 0.218  0.782
# or via gather_emmeans_draws()
## mckeon.em <- mckeon.brm4 |>
##     emmeans(~SYMBIONT, type='link') |>
##     pairs() |>
##     gather_emmeans_draws() |>
##     mutate(Eff=exp(.value),
##            PEff=100*(Eff-1))#,
##              # Prob = plogis(.value))
## mckeon.em |> head()
## mckeon.em |>
##     group_by(contrast) |>
##     dplyr::select(contrast, Eff) |>
##     median_hdi()
## mckeon.em |>
##   group_by(contrast) |>
##   summarize(Prob=sum(Eff>1)/n())

## On a probability scale
mckeon.em <- mckeon.brm4 |>
  emmeans(~SYMBIONT, type = "link") |>
  regrid() |>
  pairs() |>
  gather_emmeans_draws() |>
  mutate(Eff = .value) # ,
Warning: Method 'parse_bf' is deprecated. Please use 'brmsterms' instead.
mckeon.em |> head()
# A tibble: 6 × 6
# Groups:   contrast [1]
  contrast     .chain .iteration .draw  .value     Eff
  <chr>         <int>      <int> <int>   <dbl>   <dbl>
1 none - crabs     NA         NA     1 -0.0265 -0.0265
2 none - crabs     NA         NA     2 -0.0328 -0.0328
3 none - crabs     NA         NA     3 -0.127  -0.127 
4 none - crabs     NA         NA     4  0.0374  0.0374
5 none - crabs     NA         NA     5  0.643   0.643 
6 none - crabs     NA         NA     6 -0.122  -0.122 
mckeon.em |>
  group_by(contrast) |>
  dplyr::select(contrast, Eff) |>
  median_hdi()
# A tibble: 6 × 7
  contrast          Eff  .lower .upper .width .point .interval
  <chr>           <dbl>   <dbl>  <dbl>  <dbl> <chr>  <chr>    
1 crabs - both   0.307  -0.209   0.845   0.95 median hdi      
2 crabs - shrimp 0.0822 -0.471   0.586   0.95 median hdi      
3 none - both    0.494   0.0344  0.885   0.95 median hdi      
4 none - crabs   0.121  -0.114   0.643   0.95 median hdi      
5 none - shrimp  0.245  -0.0656  0.746   0.95 median hdi      
6 shrimp - both  0.206  -0.313   0.726   0.95 median hdi      
## Cell means
mckeon.em <- emmeans(mckeon.brm4, ~SYMBIONT, type = "link") |>
  gather_emmeans_draws()
Warning: Method 'parse_bf' is deprecated. Please use 'brmsterms' instead.
mckeon.em |>
  mutate(P = plogis(.value)) |>
  median_hdci(P)
# A tibble: 4 × 7
  SYMBIONT     P .lower .upper .width .point .interval
  <fct>    <dbl>  <dbl>  <dbl>  <dbl> <chr>  <chr>    
1 none     0.951 0.798   0.999   0.95 median hdci     
2 crabs    0.818 0.320   1.000   0.95 median hdci     
3 shrimp   0.693 0.204   0.998   0.95 median hdci     
4 both     0.438 0.0184  0.887   0.95 median hdci     
cmat <- cbind(
  "Crab vs shrimp" = c(0, 1, -1, 0),
  "Both vs One" = c(0, -1 / 2, -1 / 2, 1),
  "Any vs None" = c(-1, 1 / 3, 1 / 3, 1 / 3)
)

mckeon.em <- mckeon.brm4 |>
  emmeans(~SYMBIONT, type = "response") |>
  contrast(method = list(cmat))
Warning: Method 'parse_bf' is deprecated. Please use 'brmsterms' instead.
mckeon.em <-
  mckeon.brm4 |>
  emmeans(~SYMBIONT, type = "link") |>
  contrast(method = list(cmat)) |>
  tidy_draws() |>
  mutate(across(starts_with("contrast"), exp)) |>
  summarise_draws(median,
    HDInterval::hdi,
    Pl = ~ mean(.x < 1),
    Pg = ~ mean(.x > 1)
  )
Warning: Method 'parse_bf' is deprecated. Please use 'brmsterms' instead.
mckeon.em
# A tibble: 3 × 6
  variable                median   lower  upper    Pl     Pg
  <chr>                    <dbl>   <dbl>  <dbl> <dbl>  <dbl>
1 contrast Crab vs shrimp 1.95   0.00104 45.7   0.334 0.666 
2 contrast Both vs One    0.236  0.00104  2.13  0.887 0.113 
3 contrast Any vs None    0.0933 0.00125  0.685 0.971 0.0288
## or via gather_emmeans_draws
mckeon.em <-
  mckeon.brm4 |>
  emmeans(~SYMBIONT, type = "link") |>
  contrast(method = list(cmat)) |>
  gather_emmeans_draws() |>
  mutate(Fit = exp(.value)) |>
  summarise(median_hdci(Fit),
    Pl =  mean(Fit < 0),
    Pg =  mean(Fit > 0)
  )
Warning: Method 'parse_bf' is deprecated. Please use 'brmsterms' instead.
mckeon.em
# A tibble: 3 × 9
  contrast            y    ymin   ymax .width .point .interval    Pl    Pg
  <chr>           <dbl>   <dbl>  <dbl>  <dbl> <chr>  <chr>     <dbl> <dbl>
1 Any vs None    0.0933 0.00289  0.689   0.95 median hdci          0     1
2 Both vs One    0.236  0.00104  2.13    0.95 median hdci          0     1
3 Crab vs shrimp 1.95   0.00104 45.5     0.95 median hdci          0     1
newdata <- emmeans(mckeon.brm4, ~SYMBIONT, type = "response") |> as.data.frame()
Warning: Method 'parse_bf' is deprecated. Please use 'brmsterms' instead.
head(newdata)
 SYMBIONT  response lower.HPD upper.HPD
 none     0.9511953 0.8018662 0.9997149
 crabs    0.8177095 0.3196022 0.9997197
 shrimp   0.6926152 0.2037201 0.9975018
 both     0.4384410 0.0183692 0.8872965

Point estimate displayed: median 
Results are back-transformed from the logit scale 
HPD interval probability: 0.95 
ggplot(newdata, aes(y = response, x = SYMBIONT)) +
  geom_pointrange(aes(ymin = lower.HPD, ymax = upper.HPD))

mckeon.brm4 |> bayes_R2(re.form = NA)
    Estimate  Est.Error       Q2.5     Q97.5
R2 0.1698209 0.09045373 0.01086722 0.3231581
mckeon.brm4 |>
  bayes_R2(re.form = NA, summary = FALSE) |>
  median_hdci()
         y        ymin      ymax .width .point .interval
1 0.174463 0.004768317 0.3150586   0.95 median      hdci
mckeon.brm4 |>
  bayes_R2(re.form = ~ (1 | BLOCK), summary = FALSE) |>
  median_hdci()
          y      ymin     ymax .width .point .interval
1 0.4774058 0.1171811 0.730853   0.95 median      hdci
## for random intercept/slope model
mckeon.brm4 |>
  bayes_R2(re.form = ~ (SYMBIONT | BLOCK), summary = FALSE) |>
  median_hdci()
          y      ymin      ymax .width .point .interval
1 0.7037594 0.5693019 0.8187142   0.95 median      hdci

11 References

Mckeon, Seabird, Adrian Stier, Shelby Mcilroy, and Benjamin Bolker. 2012. “Multiple Defender Effects: Synergistic Coral Defense by Mutualist Crustaceans.” Oecologia 169 (February): 1095–1103. https://doi.org/10.1007/s00442-012-2275-2.